def __init__(self): super().__init__() hybridlogger.ha_log(self.logger, self.hass_api, "INFO", "Client enabled: TCPclient") # Get plant_id_list self.plant_id_list = self.config.getlist("client.tcpclient", "plant_id_list", fallback=[]) if not self.plant_id_list: hybridlogger.ha_log( self.logger, self.hass_api, "ERROR", "plant_id_list was not specified for client TCPclient", ) raise Exception("plant_id_list was not specified") # Initialize dict with inverter info self.inverters = {} self.session = {} # self._mode indicates the access mode that will be used # 0: Not initialized # 1: Running in native mode (port 8899) # 2: Running in fallback mode (port 80) fetching http://{inverter_ip}:80/js/status.js self._mode = {}
def getPlantData(self, plant_id): data = None http_only = self.inverters[plant_id].get("http_only") if not self._mode.get(plant_id) and not http_only: # native mode hybridlogger.ha_log( self.logger, self.hass_api, "INFO", f"Initializing: Trying to reach the inverter for plant {plant_id} over port 8899.", ) if (self._mode.get(plant_id) <= 1 and self.inverters[plant_id].get("inverter_sn") and self.inverters[plant_id].get("logger_sn") and not http_only): data = self._getPlantData_native(plant_id) if data: # set mode to native self._mode[plant_id] = 1 if not self._mode: hybridlogger.ha_log( self.logger, self.hass_api, "INFO", f"Initializing: Trying to reach the inverter for plant {plant_id} over port http (fallback).", ) if self._mode.get(plant_id) == 0 or self._mode.get(plant_id) == 2: # fall back mode data = self._getPlantData_fallback(plant_id) return data
def _run(self, entity, attribute, old, new, kwargs): # HASSAPI callback handler hybridlogger.ha_log( self.logger, self.hass_api, "DEBUG", f"HASSapi state change for {self.hass_api.get_state(entity, 'inverter', 'n/a')} " f"at {self.hass_api.get_state(entity, 'last_update', 'n/a')}", ) # Try to parse payload as json try: data = binascii.a2b_base64(new) if len(data) == 128: # Fill data structure self.client.semaphore.acquire() self.client.msg["data"] = data self.client.msg["isSet"] = True self.client.msg["plugin"] = __name__ self.client.semaphore.release() # Trigger processing the message self.client.msgevent.set() except Exception as e: hybridlogger.ha_log( self.logger, self.hass_api, "WARNING", f"HASSapi: invalid data received: {new}. Error {e}", )
def __init__(self): super().__init__() hybridlogger.ha_log(self.logger, self.hass_api, "INFO", "localproxy client plugin: MQTTproxy") self.logger_sensor_name = self.mqttconfig('logger_sensor_name', 'Datalogger') self.mqtt_discovery_prefix = self.mqttconfig('listen_address', 'homeassistant') self.mqtt_host = self.mqttconfig('host', 'localhost') self.mqtt_port = int(self.mqttconfig('port', '1883')) self.mqtt_client_name_prefix = self.mqttconfig( 'client_name_prefix', 'ha-mqttproxy-omniklogger') self.mqtt_client_name = self.mqtt_client_name_prefix + "_" + uuid.uuid4( ).hex self.mqtt_username = self.mqttconfig('username', None) self.mqtt_password = self.mqttconfig('password', None) if not self.mqtt_username or not self.mqtt_password: hybridlogger.ha_log( self.logger, self.hass_api, "ERROR", "Please specify MQTT username and password in the configuration" ) # mqtt setup self.mqtt_client = mqttclient.Client(self.mqtt_client_name) self.mqtt_client.on_connect = self._mqtt_on_connect # bind call back function self.mqtt_client.on_disconnect = self._mqtt_on_disconnect # bind call back function self.mqtt_client.on_message = self._mqtt_on_message # called on receiving updates on subscibed messages self.mqtt_client.username_pw_set(self.mqtt_username, self.mqtt_password)
def get_weather(self): try: if "weather" not in self.cache: self.logger.debug("[cache miss] Fetching weather data") url = "https://{endpoint}/data/2.5/weather?lon={lon}&lat={lat}&units={units}&APPID={api_key}".format( endpoint=self.config.get( "openweathermap", "endpoint", fallback="api.openweathermap.org"), lat=self.config.get("openweathermap", "lat"), lon=self.config.get("openweathermap", "lon"), units=self.config.get("openweathermap", "units", fallback="metric"), api_key=self.config.get("openweathermap", "api_key"), ) res = requests.get(url) res.raise_for_status() self.cache["weather"] = res.json() return self.cache["weather"] except requests.exceptions.HTTPError as e: hybridlogger.ha_log( self.logger, self.hass_api, "ERROR", "Unable to get weather data. [{0}]: {1}".format( type(e).__name__, str(e)), ) raise e
def _validate_user_login(self): self.omnik_api_level = 0 try: self.client.initialize() # Logged on self.omnik_api_level = 1 except requests.exceptions.RequestException as err: hybridlogger.ha_log( self.logger, self.hass_api, "WARNING", f"Request error during account validation omnik portal: {err}") except requests.exceptions.HTTPError as errh: hybridlogger.ha_log( self.logger, self.hass_api, "WARNING", f"HTTP error during account validation omnik portal: {errh}") except requests.exceptions.ConnectionError as errc: hybridlogger.ha_log( self.logger, self.hass_api, "WARNING", f"Connection error during account validation omnik portal: {errc}" ) except requests.exceptions.Timeout as errt: hybridlogger.ha_log( self.logger, self.hass_api, "WARNING", f"Timeout error during account validation omnik portal: {errt}" ) except Exception as e: hybridlogger.ha_log(self.logger, self.hass_api, "ERROR", e)
def _fetch_plants(self): # Fetch te plant's available for the account if (not self.plant_update or self.omnik_api_level == 1): try: plants = self.client.getPlants() for pid in plants: self.plant_update[str( pid['plant_id'])] = self.last_update_time self.omnik_api_level = 2 except requests.exceptions.RequestException as err: hybridlogger.ha_log(self.logger, self.hass_api, "WARNING", f"Request error: {err}") self.omnik_api_level = 0 return False except requests.exceptions.HTTPError as errh: hybridlogger.ha_log(self.logger, self.hass_api, "WARNING", f"HTTP error: {errh}") self.omnik_api_level = 0 return False except requests.exceptions.ConnectionError as errc: hybridlogger.ha_log(self.logger, self.hass_api, "WARNING", f"Connection error: {errc}") self.omnik_api_level = 0 return False except requests.exceptions.Timeout as errt: hybridlogger.ha_log(self.logger, self.hass_api, "WARNING", f"Timeout error: {errt}") self.omnik_api_level = 0 return False except Exception as e: hybridlogger.ha_log(self.logger, self.hass_api, "ERROR", e) self.omnik_api_level = 0 return False return True
def getPlants(self): # Get station list url = f"{self.base_url}/station/v1.0/list" json = {} stationlist = self._api_request(url, json=json) if not stationlist or not stationlist.get("success"): hybridlogger.ha_log( self.logger, self.hass_api, "WARNING", "plant/station list cannot be loaded from the cloud config, no valid data available", ) return None # Get INVERTER devices for retreived stations data = [] for station in stationlist.get("stationList"): # Only append stations that have a valid INVERTER device url = f"{self.base_url}/station/v1.0/device" json = {"stationId": station.get("id")} devicelist = self._api_request(url, json=json) for device in devicelist.get("deviceListItems"): if device.get("deviceType") == "INVERTER": data.append({ "plant_id": f'{str(station.get("id"))},{str(device.get("deviceSn"))}', }) return data
def terminate(self): hybridlogger.ha_log(logger, self, "INFO", "Stopping Omnikdatalogger...") self.rt.stop() self.datalogger.terminate() hybridlogger.ha_log(logger, self, "INFO", "Omnikdatalogger was stopped")
def get_weather(self): try: if 'weather' not in self.cache: self.logger.debug('[cache miss] Fetching weather data') url = "https://{endpoint}/data/2.5/weather?lon={lon}&lat={lat}&units={units}&APPID={api_key}".format( endpoint=self.config.get( 'openweathermap', 'endpoint', fallback='api.openweathermap.org'), lat=self.config.get('openweathermap', 'lat'), lon=self.config.get('openweathermap', 'lon'), units=self.config.get('openweathermap', 'units', fallback='metric'), api_key=self.config.get('openweathermap', 'api_key'), ) res = requests.get(url) res.raise_for_status() self.cache['weather'] = res.json() return self.cache['weather'] except requests.exceptions.HTTPError as e: hybridlogger.ha_log( self.logger, self.hass_api, "ERROR", 'Unable to get weather data. [{0}]: {1}'.format( type(e).__name__, str(e))) raise e
def getPlants(self): # Get the plant neeeded data from config data = [] inverterdata = {} for plant in self.plant_id_list: inverter_sn = self.config.get(f"plant.{plant}", "inverter_sn", fallback=None) if not inverter_sn: hybridlogger.ha_log( self.logger, self.hass_api, "ERROR", "inverter_sn (The serial number of the inverter) for " f"plant {plant} was not specified for [TCPclient]", ) raise Exception( "inverter_sn (a serial number of the inverter) was not specified" ) inverterdata = {"inverter_sn": inverter_sn} self.inverters[plant] = inverterdata data.append({"plant_id": plant}) hybridlogger.ha_log(self.logger, self.hass_api, "DEBUG", f"plant list from config {data}") # The config was read, start listening # Claim the semaphore self.semaphore.acquire() for plugin in LocalProxyPlugin.localproxy_plugins: plugin.listen() # Release the semaphore self.semaphore.release() return data
def _get_dsmr_data(self, plant, data): # if dsmr measurements are not enabled then return if not self.dsmr: return # Try to merge with the latest dsmr data available complete = False tries = 20 while tries > 0: self.dsmr_access.acquire() if plant in self.dsmr_data: # Insert raw DSMR data data.update(self._dsmr_cache(plant, data['last_update'])) # Insert calculated netto values (solar - net) self._calculate_consumption(data) complete = True self.dsmr_access.release() tries -= 1 if not complete: time.sleep(1) else: break if not tries: hybridlogger.ha_log( self.logger, self.hass_api, "WARNING", f"Could not combine DSMR data for plant {plant}. " "Did you configure plant_id at your dsmr terminal config?")
def __init__(self, config, logger, hass_api, terminal_name, dsmr_serial_callback, dsmr_version): self.config = config self.logger = logger self.hass_api = hass_api self.terminal_name = terminal_name self.dsmr_serial_callback = dsmr_serial_callback self.mode = self.config.get(f"dsmr.{self.terminal_name}", 'mode', 'device') if self.mode not in ['tcp', 'device']: hybridlogger.ha_log( self.logger, self.hass_api, "ERROR", f"DSMR terminal {self.terminal_name} mode {self.mode} is not valid. " "Should be tcp or device. Ignoring DSMR configuration!") return self.device = self.config.get(f"dsmr.{self.terminal_name}", 'device', '/dev/ttyUSB0') self.host = self.config.get(f"dsmr.{self.terminal_name}", 'host', 'localhost') self.port = self.config.get(f"dsmr.{self.terminal_name}", 'port', '3333') self.dsmr_version = dsmr_version # start terminal self.stop = False hybridlogger.ha_log( self.logger, self.hass_api, "INFO", f"Initializing DSMR termimal '{terminal_name}'. Mode: {self.mode}." ) if self.mode == 'tcp': self.thr = threading.Thread(target=self._run_tcp_terminal, name=self.terminal_name) elif self.mode == 'device': self.thr = threading.Thread(target=self._run_serial_terminal, name=self.terminal_name) self.thr.start()
def getlist(self, section, option, fallback=[], **kwargs): if str(section).lower() == "default": if option in self.ha_args: payload = self.ha_args.get(option, fallback) if isinstance(payload, list): return payload else: hybridlogger.ha_log( logger, self, "ERROR", f"Config type error: Section: '{section}', Attribute: '{option}'; Expected <class 'list'> got {str(type(payload))}", ) return fallback else: if str(section) in self.ha_args: if option in self.ha_args[section]: payload = self.ha_args[section].get(option, fallback) if isinstance(payload, list): return payload else: hybridlogger.ha_log( logger, self, "ERROR", f"Config type error: Section: '{section}'; Attribute: '{option}'; Expected <class 'list'> got {str(type(payload))}", ) return fallback try: retval = super().get(section, option, fallback=fallback, **kwargs) except Exception: retval = fallback pass return retval
def getPlants(self): url = f'{self.base_url}/plant/list' data = self._api_request(url, 'GET', None) hybridlogger.ha_log(self.logger, self.hass_api, "DEBUG", f"plant list {data}") return data['data'].get('plants', [])
def __init__(self): super().__init__() hybridlogger.ha_log(self.logger, self.hass_api, "INFO", "localproxy client plugin: HASSAPI") self.hass_handle = None if not self.hass_api: hybridlogger.ha_log(self.logger, self.hass_api, "ERROR", "No HassAPI detected. Use AppDaemon with Home Assistent for this plugin") return self.logger_entity = self.config.get('client.localproxy.hassapi', 'logger_entity', 'binary_sensor.datalogger')
def log_available_fields(self, msg={}): """Logs the available output fields to the debug log. This helps to determine waht fields are available.""" # Log available fields hybridlogger.ha_log( self.logger, self.hass_api, "DEBUG", f"Output for '{self.name}'. Fields: {list(msg.keys())}", )
def _output_update(self, plant, data): # Insert dummy data for fields that have not been supplied by the client data = self._validate_client_data(plant, data) # Process for each plugin, but only when valid for plugin in Plugin.plugins: if not plugin.process_aggregates or 'sys_id' in data: hybridlogger.ha_log( self.logger, self.hass_api, "DEBUG", f"Trigger plugin '{getattr(plugin, 'name')}'.") plugin.process(msg=data)
def getPlants(self): data = [] for plant in self.config.getlist('client.solarmanpv', 'plant_id_list'): data.append({'plant_id': plant}) hybridlogger.ha_log(self.logger, self.hass_api, "DEBUG", f"plant list from config {data}") return data
def terminate(self): try: # Shutting down tcp server self.tcpServer.shutdown() except Exception as e: hybridlogger.ha_log( self.logger, self.hass_api, "WARNING", f"Error shutting down tcp_proxy server. Error: {e}.") # exit the hard way! os.sys.exit(1)
def _init_terminals(self, terminals): self.terminals = {} self.sync = {} self.cache = {} self.ts_last_telegram = {} self.last_gas_update = {} self.tconfig = {} self.tconfig['tarif'] = {} if self.config.has_option('dsmr', 'tarif'): tarif_list = self.config.getlist('dsmr', 'tarif') else: tarif_list = ['0001', '0002'] self.tconfig['tarif']['0001'] = 'low' self.tconfig['tarif']['0002'] = 'normal' for tarif in tarif_list: if tarif in self.tconfig['tarif']: default = self.tconfig['tarif'][tarif] else: default = tarif self.tconfig['tarif'][tarif] = self.config.get( 'dsmr', f"tarif.{tarif}", default) for terminal in terminals: # Pre read configuration for each terminal self.tconfig[terminal] = {} self.tconfig[terminal]['plant_id'] = self.config.get( f"dsmr.{terminal}", 'plant_id', '') self.tconfig[terminal]['gas_meter'] = self.config.getboolean( f"dsmr.{terminal}", 'gas_meter', True) self.tconfig[terminal]['dsmr_version'] = self.config.get( f"dsmr.{terminal}", 'dsmr_version ', '5') self.tconfig[terminal]['total_energy_offset'] = \ Decimal(self.config.get(f"dsmr.{terminal}", 'total_energy_offset', '0')) # Init terminal sync parameters self.sync[terminal] = 0 self.cache[terminal] = Decimal(1000000) self.ts_last_telegram[terminal] = 0 self.last_gas_update[terminal] = [ 0, Decimal('0.0'), Decimal('0.000') ] # Warnings and errors if not self.tconfig[terminal]['plant_id']: hybridlogger.ha_log( self.logger, self.hass_api, "WARNING", f"DSMR 'plant_id' for terminal '{terminal}' is not specified. " "Received data will be NOT be associated with your solar data " "and will be processed as stand-a-lone data!") # Initialize terminal self.terminals[terminal] = Terminal( self.config, self.logger, self.hass_api, terminal, self.dsmr_serial_callback, self.tconfig[terminal]['dsmr_version'])
def _run(self): # TCP listen loop try: hybridlogger.ha_log( self.logger, self.hass_api, "INFO", f"Starting tcp_proxy server. listening at {self.listenaddress[0]}:{self.listenaddress[1]}." ) self.tcpServer.serve_forever() except Exception as e: hybridlogger.ha_log( self.logger, self.hass_api, "ERROR", f"Error binding to {self.listenaddress}. Error: {e}.")
def listen(self): if self.hass_api: for logger_entity in self.logger_entity: self.hass_handle[logger_entity] = self.hass_api.listen_state( self._run, logger_entity, attribute="data" ) hybridlogger.ha_log( self.logger, self.hass_api, "INFO", f"HASSapi listening to. '{logger_entity}', attribute: 'data'", )
def _sunshine_check(self): self.sundown = not self.dl.sun_shine() if self.sundown: hybridlogger.ha_log( self.logger, self.hass_api, "INFO", f"No sunshine postponing till down next dawn {self.dl.next_dawn}." ) # Send 0 Watt update return self.dl.next_dawn + timedelta(minutes=10) else: # return the last report time return value, but not when there is no sun return self.last_update_time
def _update_persistant_cache(self): try: file_cache = {} for item in self.cache: file_cache[item] = float(self.cache[item]) with open(self.persistant_cache_file, 'w') as json_file_config: json.dump(file_cache, json_file_config) except Exception as e: hybridlogger.ha_log( self.logger, self.hass_api, "ERROR", f"Cache file '{self.persistant_cache_file}' can not be written! Error: {e.args}" )
def _process_gas(self, msg_dsmr, telegram): terminal = threading.currentThread().getName() if not self.tconfig[terminal]["gas_meter"]: # Skip gas meter return try: if self.tconfig[terminal]["dsmr_version"] in ["4", "5"]: G = telegram[obis_references.HOURLY_GAS_METER_READING] msg_dsmr["gas_consumption_total"] = G.values[1]["value"] msg_dsmr["timestamp_gas"] = datetime.timestamp(G.values[0]["value"]) elif self.tconfig[terminal]["dsmr_version"] == "5B": G = telegram[obis_references.BELGIUM_HOURLY_GAS_METER_READING] msg_dsmr["gas_consumption_total"] = G.values[1]["value"] msg_dsmr["timestamp_gas"] = datetime.timestamp(G.values[0]["value"]) elif self.tconfig[terminal]["dsmr_version"] == "2.2": G = telegram[obis_references.GAS_METER_READING] msg_dsmr["gas_consumption_total"] = G.values[6]["value"] msg_dsmr["timestamp_gas"] = datetime.timestamp(G.values[0]["value"]) msg_dsmr["EQUIPMENT_IDENTIFIER_GAS"] = telegram[ obis_references.EQUIPMENT_IDENTIFIER_GAS ].value # Set last value to the cache if not self.last_gas_update[terminal][0]: self.last_gas_update[terminal][0] = msg_dsmr["timestamp_gas"] self.last_gas_update[terminal][1] = msg_dsmr["gas_consumption_total"] # Calculate gas consumption / hour self.last_gas_update[terminal] = [0, Decimal('0')] if msg_dsmr["timestamp_gas"] > self.last_gas_update[terminal][0]: msg_dsmr["gas_consumption_hour"] = ( ( msg_dsmr["gas_consumption_total"] - self.last_gas_update[terminal][1] ) * Decimal("3600") / Decimal( msg_dsmr["timestamp_gas"] - self.last_gas_update[terminal][0] ) ).quantize(Decimal("0.001")) self.last_gas_update[terminal][0] = msg_dsmr["timestamp_gas"] self.last_gas_update[terminal][1] = msg_dsmr["gas_consumption_total"] self.last_gas_update[terminal][2] = msg_dsmr["gas_consumption_hour"] else: msg_dsmr["gas_consumption_hour"] = self.last_gas_update[terminal][2] except Exception as e: hybridlogger.ha_log( self.logger, self.hass_api, "WARNING", f"DSMR gas_meter reading for terminal '{terminal}' failed. " f"Check your config: Error: {e.args}", )
def _load_persistant_cache(self): try: with open(self.persistant_cache_file) as total_energy_cache: file_cache = json.load(total_energy_cache) for item in file_cache: self.cache[item] = Decimal(f"{file_cache[item]}") hybridlogger.ha_log( self.logger, self.hass_api, "INFO", f"Using energy cache file '{self.persistant_cache_file}'.") except Exception as e: hybridlogger.ha_log( self.logger, self.hass_api, "INFO", f"Cache file '{self.persistant_cache_file}' was not used previously. Error: {e.args}" )
def initialize(self): pwhash = hashlib.md5(self.password.encode('utf-8')).hexdigest() url = f'{self.base_url}/Login?username={self.username}&password={pwhash}&key={self.api_key}' xmldata = self._api_request(url, 'GET', None) hybridlogger.ha_log(self.logger, self.hass_api, "DEBUG", f"account validation: {xmldata}") # Parsing the XML for element in ET.fromstring(xmldata): if element.tag == "userID": self.user_id = int(element.text) elif element.tag == "token": self.token = element.text
def _run(self): self.is_running = False # The function calls DataLogger.process() self.last_update_time = self.datalogger.process( *self.args, **self.kwargs) # Calculate the new timer interval if self.last_update_time: # Reset retry counter self.retries = 0 if self.last_update_time <= datetime.now(timezone.utc): # If last report time + 2x interval is less than the current time then increase self.new_report_expected_at = self.last_update_time + timedelta( seconds=self.interval) # Check if we have at least 60 seconds for the next cycle if self.new_report_expected_at + timedelta( seconds=-10) < datetime.now(timezone.utc): # No recent update or missing update: wait {interval}/5 from now() self.new_report_expected_at = datetime.now( timezone.utc) + timedelta(seconds=self.interval / 5) else: # Skipping dark period self.new_report_expected_at = self.last_update_time self.calculated_interval = (self.new_report_expected_at - datetime.now(timezone.utc)).seconds else: # An error occured calculate retry interval retry_interval = self.half_interval i = self.retries while i > 0: # Double retry interval to avoid to much traffic retry_interval *= 2 i -= 1 # Increment retry counter maximal interval between retries is half_interval * 2 * 2 * 2 = 4 intervals if self.retries < 3: self.retries += 1 # Calculate new report time self.new_report_expected_at = datetime.now( timezone.utc) + timedelta(seconds=retry_interval) self.calculated_interval = (self.new_report_expected_at - datetime.now(timezone.utc)).seconds # Make sure we have at least 15 seconds on the time to prevent a deadlocked timer loop if self.calculated_interval < 15: self.calculated_interval = 15 hybridlogger.ha_log( self.logger, self.hass_api, "DEBUG", f"new poll in {self.calculated_interval} seconds at {self.new_report_expected_at.isoformat()}.", ) self.start()
def getPlants(self): data = [] inverterdata = {} for plant in self.plant_id_list: inverter_address = self.config.get(f"plant.{plant}", "inverter_address", fallback=None) if not inverter_address: hybridlogger.ha_log( self.logger, self.hass_api, "ERROR", f"inverter_address for plant {plant} was not specified [TCPclient]", ) raise Exception("an inverter_address was not specified") inverter_port = int( self.config.get(f"plant.{plant}", "inverter_port", fallback="8899")) inverter_connection = (inverter_address, int(inverter_port)) logger_sn = self.config.get(f"plant.{plant}", "logger_sn", fallback=None) http_only = bool( self.config.get(f"plant.{plant}", "http_only", fallback=False)) if not logger_sn: hybridlogger.ha_log( self.logger, self.hass_api, "ERROR", "logger_sn (The serial number of the " "Wi-Fi datalogger) for plant {plant} was not specified for [TCPclient], http mode only", ) inverter_sn = self.config.get(f"plant.{plant}", "inverter_sn", fallback=None) if not inverter_sn: hybridlogger.ha_log( self.logger, self.hass_api, "WARNING", "inverter_sn (The serial number of the inverter) " "for plant {plant} was not specified for [TCPclient], http mode only", ) inverterdata = { "inverter_address": inverter_address, "inverter_port": inverter_port, "logger_sn": int(logger_sn), "inverter_sn": inverter_sn, "inverter_connection": inverter_connection, "http_only": http_only, } self.inverters[plant] = inverterdata self._mode[plant] = 0 data.append({"plant_id": plant}) hybridlogger.ha_log(self.logger, self.hass_api, "DEBUG", f"plant list from config {data}") return data