diff --git a/app/src/async_stream.py b/app/src/async_stream.py index ae37d7f..cb4f115 100644 --- a/app/src/async_stream.py +++ b/app/src/async_stream.py @@ -1,35 +1,37 @@ -import logging, traceback -from config import Config -#import gc +import logging +import traceback +# from config import Config +# import gc from messages import Message, hex_dump_memory logger = logging.getLogger('conn') + class AsyncStream(Message): - def __init__(self, reader, writer, addr, remote_stream, server_side: bool) -> None: + def __init__(self, reader, writer, addr, remote_stream, server_side: bool + ) -> None: super().__init__() self.reader = reader self.writer = writer self.remoteStream = remote_stream self.server_side = server_side self.addr = addr - + ''' Our puplic methods ''' async def loop(self) -> None: - + while True: try: - await self.__async_read() - - if self.unique_id: - await self.__async_write() + await self.__async_read() + + if self.unique_id: + await self.__async_write() await self.__async_forward() await self.async_publ_mqtt() - - + except (ConnectionResetError, ConnectionAbortedError, BrokenPipeError, @@ -43,12 +45,11 @@ class AsyncStream(Message): f"{traceback.format_exc()}") self.close() return - + def disc(self) -> None: logger.debug(f'in AsyncStream.disc() {self.addr}') self.writer.close() - - + def close(self): logger.debug(f'in AsyncStream.close() {self.addr}') self.writer.close() @@ -56,7 +57,6 @@ class AsyncStream(Message): # logger.info (f'AsyncStream refs: {gc.get_referrers(self)}') - ''' Our private methods ''' @@ -67,33 +67,34 @@ class AsyncStream(Message): self.read() # call read in parent class else: raise RuntimeError("Peer closed.") - + async def __async_write(self) -> None: if self._send_buffer: - hex_dump_memory(logging.INFO, f'Transmit to {self.addr}:', self._send_buffer, len(self._send_buffer)) + hex_dump_memory(logging.INFO, f'Transmit to {self.addr}:', + self._send_buffer, len(self._send_buffer)) self.writer.write(self._send_buffer) await self.writer.drain() - self._send_buffer = bytearray(0) #self._send_buffer[sent:] - + self._send_buffer = bytearray(0) # self._send_buffer[sent:] + async def __async_forward(self) -> None: if self._forward_buffer: if not self.remoteStream: - await self.async_create_remote() # only implmeneted for server side => syncServerStream - + await self.async_create_remote() + if self.remoteStream: - hex_dump_memory(logging.INFO, f'Forward to {self.remoteStream.addr}:', self._forward_buffer, len(self._forward_buffer)) - self.remoteStream.writer.write (self._forward_buffer) - await self.remoteStream.writer.drain() + hex_dump_memory(logging.INFO, + f'Forward to {self.remoteStream.addr}:', + self._forward_buffer, + len(self._forward_buffer)) + self.remoteStream.writer.write(self._forward_buffer) + await self.remoteStream.writer.drain() self._forward_buffer = bytearray(0) async def async_create_remote(self) -> None: pass async def async_publ_mqtt(self) -> None: - pass - - - def __del__ (self): - logging.debug (f"AsyncStream.__del__ {self.addr}") - + pass + def __del__(self): + logging.debug(f"AsyncStream.__del__ {self.addr}") diff --git a/app/src/config.py b/app/src/config.py index 8a4fc00..ada2f90 100644 --- a/app/src/config.py +++ b/app/src/config.py @@ -1,78 +1,90 @@ '''Config module handles the proxy configuration in the config.toml file''' -import shutil, tomllib, logging +import shutil +import tomllib +import logging from schema import Schema, And, Use, Optional + class Config(): - '''Static class Config is reads and sanitize the config. - - Read config.toml file and sanitize it with read(). + '''Static class Config is reads and sanitize the config. + + Read config.toml file and sanitize it with read(). Get named parts of the config with get()''' config = {} - conf_schema = Schema({ 'tsun': { - 'enabled': Use(bool), - 'host': Use(str), - 'port': And(Use(int), lambda n: 1024 <= n <= 65535)}, + conf_schema = Schema({ + 'tsun': { + 'enabled': Use(bool), + 'host': Use(str), + 'port': And(Use(int), lambda n: 1024 <= n <= 65535) + }, + 'mqtt': { + 'host': Use(str), + 'port': And(Use(int), lambda n: 1024 <= n <= 65535), + 'user': And(Use(str), Use(lambda s: s if len(s) > 0 else None)), + 'passwd': And(Use(str), Use(lambda s: s if len(s) > 0 else None)) + }, + 'ha': { + 'auto_conf_prefix': Use(str), + 'discovery_prefix': Use(str), + 'entity_prefix': Use(str), + 'proxy_node_id': Use(str), + 'proxy_unique_id': Use(str) + }, + 'inverters': { + 'allow_all': Use(bool), And(Use(str), lambda s: len(s) == 16): { + Optional('node_id', default=""): And(Use(str), + Use(lambda s: s + '/' + if len(s) > 0 and + s[-1] != '/' else s)), - 'mqtt': { - 'host': Use(str), - 'port': And(Use(int), lambda n: 1024 <= n <= 65535), - 'user': And(Use(str), Use(lambda s: s if len(s) >0 else None)), - 'passwd': And(Use(str), Use(lambda s: s if len(s) >0 else None))}, - - - 'ha': { - 'auto_conf_prefix': Use(str), - 'discovery_prefix': Use(str), - 'entity_prefix': Use(str), - 'proxy_node_id': Use(str), - 'proxy_unique_id': Use(str)}, - - 'inverters': { - 'allow_all' : Use(bool), - And(Use(str), lambda s: len(s) == 16 ): { - Optional('node_id', default=""): And(Use(str),Use(lambda s: s +'/' if len(s)> 0 and s[-1] != '/' else s)), - Optional('suggested_area', default=""): Use(str) - }} - }, ignore_extra_keys=True) + Optional('suggested_area', default=""): Use(str) + }} + }, ignore_extra_keys=True + ) @classmethod def read(cls) -> None: - '''Read config file, merge it with the default config and sanitize the result''' + '''Read config file, merge it with the default config + and sanitize the result''' config = {} logger = logging.getLogger('data') try: - # make the default config transparaent by copying it in the config.example file + # make the default config transparaent by copying it + # in the config.example file shutil.copy2("default_config.toml", "config/config.example.toml") # read example config file as default configuration with open("default_config.toml", "rb") as f: def_config = tomllib.load(f) - # overwrite the default values, with values from the config.toml file + # overwrite the default values, with values from + # the config.toml file with open("config/config.toml", "rb") as f: usr_config = tomllib.load(f) - - config['tsun'] = def_config['tsun'] | usr_config['tsun'] - config['mqtt'] = def_config['mqtt'] | usr_config['mqtt'] - config['ha'] = def_config['ha'] | usr_config['ha'] - config['inverters'] = def_config['inverters'] | usr_config['inverters'] + + config['tsun'] = def_config['tsun'] | usr_config['tsun'] + config['mqtt'] = def_config['mqtt'] | usr_config['mqtt'] + config['ha'] = def_config['ha'] | usr_config['ha'] + config['inverters'] = def_config['inverters'] | \ + usr_config['inverters'] cls.config = cls.conf_schema.validate(config) - #logging.debug(f'Readed config: "{cls.config}" ') - + # logging.debug(f'Readed config: "{cls.config}" ') + except Exception as error: logger.error(f'Config.read: {error}') cls.config = {} @classmethod - def get(cls, member:str = None): - '''Get a named attribute from the proxy config. If member == None it returns the complete config dict''' + def get(cls, member: str = None): + '''Get a named attribute from the proxy config. If member == + None it returns the complete config dict''' if member: return cls.config.get(member, {}) else: - return cls.config \ No newline at end of file + return cls.config diff --git a/app/src/infos.py b/app/src/infos.py index 8d58400..4e3c7af 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -1,11 +1,13 @@ -import struct, json, logging, os +import struct +import json +import logging +import os - -class Infos: +class Infos: stat = {} app_name = os.getenv('SERVICE_NAME', 'proxy') - version = os.getenv('VERSION', 'unknown') + version = os.getenv('VERSION', 'unknown') @classmethod def static_init(cls): @@ -14,157 +16,162 @@ class Infos: cls.stat['proxy'] = {} for key in cls.__info_defs: name = cls.__info_defs[key]['name'] - if name[0]=='proxy': + if name[0] == 'proxy': cls.stat['proxy'][name[1]] = 0 # add values from the environment to the device definition table prxy = cls.__info_devs['proxy'] - prxy['sw'] = cls.version + prxy['sw'] = cls.version prxy['mdl'] = cls.app_name - def __init__(self): self.db = {} self.tracer = logging.getLogger('data') - __info_devs={ - 'proxy': {'singleton': True, 'name':'Proxy', 'mf':'Stefan Allius'}, - 'controller':{'via':'proxy', 'name':'Controller', 'mdl':0x00092f90, 'mf':0x000927c0, 'sw':0x00092ba8}, - 'inverter': {'via':'controller', 'name':'Micro Inverter', 'mdl':0x00000032, 'mf':0x00000014, 'sw':0x0000001e}, - 'input_pv1': {'via':'inverter', 'name':'Module PV1'}, - 'input_pv2': {'via':'inverter', 'name':'Module PV2', 'dep':{'reg':0x00095b50, 'gte': 2}}, - 'input_pv3': {'via':'inverter', 'name':'Module PV3', 'dep':{'reg':0x00095b50, 'gte': 3}}, - 'input_pv4': {'via':'inverter', 'name':'Module PV4', 'dep':{'reg':0x00095b50, 'gte': 4}}, + __info_devs = { + 'proxy': {'singleton': True, 'name': 'Proxy', 'mf': 'Stefan Allius'}, # noqa: E501 + 'controller': {'via': 'proxy', 'name': 'Controller', 'mdl': 0x00092f90, 'mf': 0x000927c0, 'sw': 0x00092ba8}, # noqa: E501 + 'inverter': {'via': 'controller', 'name': 'Micro Inverter', 'mdl': 0x00000032, 'mf': 0x00000014, 'sw': 0x0000001e}, # noqa: E501 + 'input_pv1': {'via': 'inverter', 'name': 'Module PV1'}, + 'input_pv2': {'via': 'inverter', 'name': 'Module PV2', 'dep': {'reg': 0x00095b50, 'gte': 2}}, # noqa: E501 + 'input_pv3': {'via': 'inverter', 'name': 'Module PV3', 'dep': {'reg': 0x00095b50, 'gte': 3}}, # noqa: E501 + 'input_pv4': {'via': 'inverter', 'name': 'Module PV4', 'dep': {'reg': 0x00095b50, 'gte': 4}}, # noqa: E501 } - __info_defs={ + __info_defs = { # collector values used for device registration: - 0x00092ba8: {'name':['collector', 'Collector_Fw_Version'], 'level': logging.INFO, 'unit': ''}, - 0x000927c0: {'name':['collector', 'Chip_Type'], 'level': logging.DEBUG, 'unit': ''}, - 0x00092f90: {'name':['collector', 'Chip_Model'], 'level': logging.DEBUG, 'unit': ''}, - 0x00095a88: {'name':['collector', 'Trace_URL'], 'level': logging.DEBUG, 'unit': ''}, - 0x00095aec: {'name':['collector', 'Logger_URL'], 'level': logging.DEBUG, 'unit': ''}, - 0x00095b50: {'name':['collector', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, - + 0x00092ba8: {'name': ['collector', 'Collector_Fw_Version'], 'level': logging.INFO, 'unit': ''}, # noqa: E501 + 0x000927c0: {'name': ['collector', 'Chip_Type'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x00092f90: {'name': ['collector', 'Chip_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x00095a88: {'name': ['collector', 'Trace_URL'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x00095aec: {'name': ['collector', 'Logger_URL'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x00095b50: {'name': ['collector', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + # inverter values used for device registration: - 0x0000000a: {'name':['inverter', 'Product_Name'], 'level': logging.DEBUG, 'unit': ''}, - 0x00000014: {'name':['inverter', 'Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, - 0x0000001e: {'name':['inverter', 'Version'], 'level': logging.INFO, 'unit': ''}, - 0x00000028: {'name':['inverter', 'Serial_Number'], 'level': logging.DEBUG, 'unit': ''}, - 0x00000032: {'name':['inverter', 'Equipment_Model'], 'level': logging.DEBUG, 'unit': ''}, - + 0x0000000a: {'name': ['inverter', 'Product_Name'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x00000014: {'name': ['inverter', 'Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x0000001e: {'name': ['inverter', 'Version'], 'level': logging.INFO, 'unit': ''}, # noqa: E501 + 0x00000028: {'name': ['inverter', 'Serial_Number'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x00000032: {'name': ['inverter', 'Equipment_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + # proxy: - 0xffffff00: {'name':['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha':{'dev':'proxy', 'comp' : 'sensor', 'dev_cla': None, 'stat_cla': None, 'id':'inv_count_', 'fmt':'| int', 'name': 'Active Inverter Connections', 'icon':'mdi:counter'}}, - 0xffffff01: {'name':['proxy', 'Unknown_SNR'], 'singleton': True, 'ha':{'dev':'proxy', 'comp' : 'sensor', 'dev_cla': None, 'stat_cla': None, 'id':'unknown_snr_', 'fmt':'| int', 'name': 'Unknown Serial No', 'icon':'mdi:counter', 'ent_cat':'diagnostic'}}, - 0xffffff02: {'name':['proxy', 'Unknown_Msg'], 'singleton': True, 'ha':{'dev':'proxy', 'comp' : 'sensor', 'dev_cla': None, 'stat_cla': None, 'id':'unknown_msg_', 'fmt':'| int', 'name': 'Unknown Msg Type', 'icon':'mdi:counter', 'ent_cat':'diagnostic'}}, - 0xffffff03: {'name':['proxy', 'Invalid_Data_Type'], 'singleton': True, 'ha':{'dev':'proxy', 'comp' : 'sensor', 'dev_cla': None, 'stat_cla': None, 'id':'inv_data_type_', 'fmt':'| int', 'name': 'Invalid Data Type', 'icon':'mdi:counter', 'ent_cat':'diagnostic'}}, - 0xffffff04: {'name':['proxy', 'Internal_Error'], 'singleton': True, 'ha':{'dev':'proxy', 'comp' : 'sensor', 'dev_cla': None, 'stat_cla': None, 'id':'intern_err_', 'fmt':'| int', 'name': 'Internal Error', 'icon':'mdi:counter', 'ent_cat':'diagnostic', 'en':False}}, - 0xffffff05: {'name':['proxy', 'Unknown_Ctrl'], 'singleton': True, 'ha':{'dev':'proxy', 'comp' : 'sensor', 'dev_cla': None, 'stat_cla': None, 'id':'unknown_ctrl_', 'fmt':'| int', 'name': 'Unknown Control Type', 'icon':'mdi:counter', 'ent_cat':'diagnostic'}}, -# 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':'| float','name': 'Grid Voltage'}}, + 0xffffff00: {'name': ['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_count_', 'fmt': '| int', 'name': 'Active Inverter Connections', 'icon': 'mdi:counter'}}, # noqa: E501 + 0xffffff01: {'name': ['proxy', 'Unknown_SNR'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_snr_', 'fmt': '| int', 'name': 'Unknown Serial No', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0xffffff02: {'name': ['proxy', 'Unknown_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_msg_', 'fmt': '| int', 'name': 'Unknown Msg Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0xffffff03: {'name': ['proxy', 'Invalid_Data_Type'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_data_type_', 'fmt': '| int', 'name': 'Invalid Data Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0xffffff04: {'name': ['proxy', 'Internal_Error'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'intern_err_', 'fmt': '| int', 'name': 'Internal Error', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic', 'en': False}}, # noqa: E501 + 0xffffff05: {'name': ['proxy', 'Unknown_Ctrl'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_ctrl_', 'fmt': '| int', 'name': 'Unknown Control Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 + # 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':'| float','name': 'Grid Voltage'}}, # noqa: E501 # events - 0x00000191: {'name':['events', '401_'], 'level': logging.DEBUG, 'unit': ''}, - 0x00000192: {'name':['events', '402_'], 'level': logging.DEBUG, 'unit': ''}, - 0x00000193: {'name':['events', '403_'], 'level': logging.DEBUG, 'unit': ''}, - 0x00000194: {'name':['events', '404_'], 'level': logging.DEBUG, 'unit': ''}, - 0x00000195: {'name':['events', '405_'], 'level': logging.DEBUG, 'unit': ''}, - 0x00000196: {'name':['events', '406_'], 'level': logging.DEBUG, 'unit': ''}, - 0x00000197: {'name':['events', '407_'], 'level': logging.DEBUG, 'unit': ''}, - 0x00000198: {'name':['events', '408_'], 'level': logging.DEBUG, 'unit': ''}, - 0x00000199: {'name':['events', '409_'], 'level': logging.DEBUG, 'unit': ''}, - 0x0000019a: {'name':['events', '410_'], 'level': logging.DEBUG, 'unit': ''}, - 0x0000019b: {'name':['events', '411_'], 'level': logging.DEBUG, 'unit': ''}, - 0x0000019c: {'name':['events', '412_'], 'level': logging.DEBUG, 'unit': ''}, - 0x0000019d: {'name':['events', '413_'], 'level': logging.DEBUG, 'unit': ''}, - 0x0000019e: {'name':['events', '414_'], 'level': logging.DEBUG, 'unit': ''}, - 0x0000019f: {'name':['events', '415_GridFreqOverRating'], 'level': logging.DEBUG, 'unit': ''}, - 0x000001a0: {'name':['events', '416_'], 'level': logging.DEBUG, 'unit': ''}, - - # grid measures: - 0x000003e8: {'name':['grid', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'inverter', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'out_volt_', 'fmt':'| float','name': 'Grid Voltage','ent_cat':'diagnostic'}}, - 0x0000044c: {'name':['grid', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'inverter', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'out_cur_', 'fmt':'| float','name': 'Grid Current','ent_cat':'diagnostic'}}, - 0x000004b0: {'name':['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha':{'dev':'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id':'out_freq_', 'fmt':'| float','name': 'Grid Frequency','ent_cat':'diagnostic'}}, - 0x00000640: {'name':['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha':{'dev':'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'out_power_', 'fmt':'| float','name': 'Power'}}, - 0x000005dc: {'name':['env', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha':{'dev':'inverter', 'dev_cla': None, 'stat_cla': None, 'id':'rated_power_', 'fmt':'| int', 'name': 'Rated Power','ent_cat':'diagnostic'}}, - 0x00000514: {'name':['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha':{'dev':'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id':'temp_', 'fmt':'| int','name': 'Temperature'}}, + 0x00000191: {'name': ['events', '401_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x00000192: {'name': ['events', '402_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x00000193: {'name': ['events', '403_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x00000194: {'name': ['events', '404_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x00000195: {'name': ['events', '405_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x00000196: {'name': ['events', '406_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x00000197: {'name': ['events', '407_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x00000198: {'name': ['events', '408_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x00000199: {'name': ['events', '409_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x0000019a: {'name': ['events', '410_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x0000019b: {'name': ['events', '411_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x0000019c: {'name': ['events', '412_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x0000019d: {'name': ['events', '413_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x0000019e: {'name': ['events', '414_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x0000019f: {'name': ['events', '415_GridFreqOverRating'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + 0x000001a0: {'name': ['events', '416_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - # input measures: - 0x000006a4: {'name':['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv1_', 'val_tpl' :"{{ (value_json['pv1']['Voltage'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}}, - 0x00000708: {'name':['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv1_', 'val_tpl' :"{{ (value_json['pv1']['Current'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}}, - 0x0000076c: {'name':['input', 'pv1', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha':{'dev':'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv1_','val_tpl' :"{{ (value_json['pv1']['Power'] | float)}}"}}, - 0x000007d0: {'name':['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv2_', 'val_tpl' :"{{ (value_json['pv2']['Voltage'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}}, - 0x00000834: {'name':['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv2_', 'val_tpl' :"{{ (value_json['pv2']['Current'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}}, - 0x00000898: {'name':['input', 'pv2', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha':{'dev':'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv2_','val_tpl' :"{{ (value_json['pv2']['Power'] | float)}}"}}, - 0x000008fc: {'name':['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv3_', 'val_tpl' :"{{ (value_json['pv3']['Voltage'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}}, - 0x00000960: {'name':['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv3_', 'val_tpl' :"{{ (value_json['pv3']['Current'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}}, - 0x000009c4: {'name':['input', 'pv3', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha':{'dev':'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv3_','val_tpl' :"{{ (value_json['pv3']['Power'] | float)}}"}}, - 0x00000a28: {'name':['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv4_', 'val_tpl' :"{{ (value_json['pv4']['Voltage'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}}, - 0x00000a8c: {'name':['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv4_', 'val_tpl' :"{{ (value_json['pv4']['Current'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}}, - 0x00000af0: {'name':['input', 'pv4', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha':{'dev':'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv4_','val_tpl' :"{{ (value_json['pv4']['Power'] | float)}}"}}, - 0x00000c1c: {'name':['input', 'pv1', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id':'daily_gen_pv1_','name': 'Daily Generation', 'val_tpl' :"{{ (value_json['pv1']['Daily_Generation'] | float)}}", 'icon':'mdi:solar-power-variant', 'must_incr':True}}, - 0x00000c80: {'name':['input', 'pv1', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total', 'id':'total_gen_pv1_','name': 'Total Generation', 'val_tpl' :"{{ (value_json['pv1']['Total_Generation'] | float)}}", 'icon':'mdi:solar-power', 'must_incr':True}}, - 0x00000ce4: {'name':['input', 'pv2', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id':'daily_gen_pv2_','name': 'Daily Generation', 'val_tpl' :"{{ (value_json['pv2']['Daily_Generation'] | float)}}", 'icon':'mdi:solar-power-variant', 'must_incr':True}}, - 0x00000d48: {'name':['input', 'pv2', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total', 'id':'total_gen_pv2_','name': 'Total Generation', 'val_tpl' :"{{ (value_json['pv2']['Total_Generation'] | float)}}", 'icon':'mdi:solar-power', 'must_incr':True}}, - 0x00000dac: {'name':['input', 'pv3', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id':'daily_gen_pv3_','name': 'Daily Generation', 'val_tpl' :"{{ (value_json['pv3']['Daily_Generation'] | float)}}", 'icon':'mdi:solar-power-variant', 'must_incr':True}}, - 0x00000e10: {'name':['input', 'pv3', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total', 'id':'total_gen_pv3_','name': 'Total Generation', 'val_tpl' :"{{ (value_json['pv3']['Total_Generation'] | float)}}", 'icon':'mdi:solar-power', 'must_incr':True}}, - 0x00000e74: {'name':['input', 'pv4', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id':'daily_gen_pv4_','name': 'Daily Generation', 'val_tpl' :"{{ (value_json['pv4']['Daily_Generation'] | float)}}", 'icon':'mdi:solar-power-variant', 'must_incr':True}}, - 0x00000ed8: {'name':['input', 'pv4', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total', 'id':'total_gen_pv4_','name': 'Total Generation', 'val_tpl' :"{{ (value_json['pv4']['Total_Generation'] | float)}}", 'icon':'mdi:solar-power', 'must_incr':True}}, - # total: - 0x00000b54: {'name':['total', 'Daily_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha':{'dev':'inverter', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id':'daily_gen_', 'fmt':'| float','name': 'Daily Generation', 'icon':'mdi:solar-power-variant', 'must_incr':True}}, - 0x00000bb8: {'name':['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha':{'dev':'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id':'total_gen_', 'fmt':'| float','name': 'Total Generation', 'icon':'mdi:solar-power', 'must_incr':True}}, + # grid measures: + 0x000003e8: {'name': ['grid', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'inverter', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'out_volt_', 'fmt': '| float', 'name': 'Grid Voltage', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0x0000044c: {'name': ['grid', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'inverter', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'out_cur_', 'fmt': '| float', 'name': 'Grid Current', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0x000004b0: {'name': ['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha': {'dev': 'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id': 'out_freq_', 'fmt': '| float', 'name': 'Grid Frequency', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0x00000640: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': '| float', 'name': 'Power'}}, # noqa: E501 + 0x000005dc: {'name': ['env', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'fmt': '| int', 'name': 'Rated Power', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0x00000514: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': '| int', 'name': 'Temperature'}}, # noqa: E501 + + # input measures: + 0x000006a4: {'name': ['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv1_', 'val_tpl': "{{ (value_json['pv1']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0x00000708: {'name': ['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv1_', 'val_tpl': "{{ (value_json['pv1']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0x0000076c: {'name': ['input', 'pv1', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv1_', 'val_tpl': "{{ (value_json['pv1']['Power'] | float)}}"}}, # noqa: E501 + 0x000007d0: {'name': ['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv2_', 'val_tpl': "{{ (value_json['pv2']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0x00000834: {'name': ['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv2_', 'val_tpl': "{{ (value_json['pv2']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0x00000898: {'name': ['input', 'pv2', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv2_', 'val_tpl': "{{ (value_json['pv2']['Power'] | float)}}"}}, # noqa: E501 + 0x000008fc: {'name': ['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv3_', 'val_tpl': "{{ (value_json['pv3']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0x00000960: {'name': ['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv3_', 'val_tpl': "{{ (value_json['pv3']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0x000009c4: {'name': ['input', 'pv3', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv3_', 'val_tpl': "{{ (value_json['pv3']['Power'] | float)}}"}}, # noqa: E501 + 0x00000a28: {'name': ['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv4_', 'val_tpl': "{{ (value_json['pv4']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0x00000a8c: {'name': ['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv4_', 'val_tpl': "{{ (value_json['pv4']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0x00000af0: {'name': ['input', 'pv4', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv4_', 'val_tpl': "{{ (value_json['pv4']['Power'] | float)}}"}}, # noqa: E501 + 0x00000c1c: {'name': ['input', 'pv1', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv1_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv1']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 + 0x00000c80: {'name': ['input', 'pv1', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv1_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv1']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 + 0x00000ce4: {'name': ['input', 'pv2', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv2_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv2']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 + 0x00000d48: {'name': ['input', 'pv2', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv2_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv2']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 + 0x00000dac: {'name': ['input', 'pv3', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv3_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv3']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 + 0x00000e10: {'name': ['input', 'pv3', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv3_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv3']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 + 0x00000e74: {'name': ['input', 'pv4', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv4_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv4']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 + 0x00000ed8: {'name': ['input', 'pv4', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv4_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv4']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 + # total: + 0x00000b54: {'name': ['total', 'Daily_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_', 'fmt': '| float', 'name': 'Daily Generation', 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 + 0x00000bb8: {'name': ['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': '| float', 'name': 'Total Generation', 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 # controller: - 0x000c3500: {'name':['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%' , 'ha':{'dev':'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id':'signal_', 'fmt':'| int', 'name': 'Signal Strength', 'icon':'mdi:wifi'}}, - 0x000c96a8: {'name':['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha':{'dev':'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id':'power_on_time_', 'name': 'Power on Time', 'val_tpl':"{{ (value_json['Power_On_Time'] | float)}}", 'nat_prc':'3','ent_cat':'diagnostic'}}, - 0x000cf850: {'name':['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha':{'dev':'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id':'data_up_intval_', 'fmt':'| int', 'name': 'Data Up Interval', 'icon':'mdi:update','ent_cat':'diagnostic'}}, + 0x000c3500: {'name': ['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': '| int', 'name': 'Signal Strength', 'icon': 'mdi:wifi'}}, # noqa: E501 + 0x000c96a8: {'name': ['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'name': 'Power on Time', 'val_tpl': "{{ (value_json['Power_On_Time'] | float)}}", 'nat_prc': '3', 'ent_cat': 'diagnostic'}}, # noqa: E501 + 0x000cf850: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'data_up_intval_', 'fmt': '| int', 'name': 'Data Up Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501 - } - - def dev_value(self, idx:str|int) -> str|int|float|None: + } + + def dev_value(self, idx: str | int) -> str | int | float | None: '''returns the stored device value from our database - idx:int ==> lookup the value in the database and return it as str, int or flout. If the value is not available return 'None' - idx:str ==> returns the string as a fixed value without a database loopup + idx:int ==> lookup the value in the database and return it as str, + int or flout. If the value is not available return 'None' + idx:str ==> returns the string as a fixed value without a + database loopup ''' - if type (idx) is str: - return idx # return idx as a fixed value + if type(idx) is str: + return idx # return idx as a fixed value elif idx in self.__info_defs: row = self.__info_defs[idx] if 'singleton' in row and row['singleton']: dict = self.stat else: dict = self.db - + keys = row['name'] for key in keys: if key not in dict: return None # value not found in the database dict = dict[key] - return dict # value of the reqeusted entry - + return dict # value of the reqeusted entry + return None # unknwon idx, not in __info_defs - def ignore_this_device(self, dep:dict) -> bool: + def ignore_this_device(self, dep: dict) -> bool: '''Checks the equation in the dep dict - - returns 'False' only if the equation is valid; 'True' in any other case''' + + returns 'False' only if the equation is valid; + 'True' in any other case''' if 'reg' in dep: value = self.dev_value(dep['reg']) - if not value: return True - + if not value: + return True + if 'gte' in dep: return not value >= dep['gte'] elif 'less_eq' in dep: return not value <= dep['less_eq'] return True - - def ha_confs(self, ha_prfx, node_id, snr, singleton:bool, sug_area =''): - '''Generator function yields a json register struct for home-assistant auto configuration and a unique entity string + + def ha_confs(self, ha_prfx, node_id, snr, singleton: bool, sug_area=''): + '''Generator function yields a json register struct for home-assistant + auto configuration and a unique entity string arguments: prfx:str ==> MQTT prefix for the home assistant 'stat_t string - snr:str ==> serial number of the inverter, used to build unique entity strings + snr:str ==> serial number of the inverter, used to build unique + entity strings sug_area:str ==> suggested area string from the config file''' tab = self.__info_defs for key in tab: @@ -173,69 +180,75 @@ class Infos: if singleton != row['singleton']: continue elif singleton: - continue + continue prfx = ha_prfx + node_id - #check if we have details for home assistant + # check if we have details for home assistant if 'ha' in row: ha = row['ha'] if 'comp' in ha: component = ha['comp'] else: component = 'sensor' - attr = {} # dict to collect all the sensor entity details + attr = {} if 'name' in ha: - attr['name'] = ha['name'] # take the entity name from the ha dict - else: - attr['name'] = row['name'][-1] # otherwise take a name from the name array + attr['name'] = ha['name'] + else: + attr['name'] = row['name'][-1] - attr['stat_t'] = prfx +row['name'][0] # eg. 'stat_t': "tsun/garagendach/grid" - attr['dev_cla'] = ha['dev_cla'] # eg. 'dev_cla': 'power' - attr['stat_cla'] = ha['stat_cla'] # eg. 'stat_cla': "measurement" - attr['uniq_id'] = ha['id']+snr # build the 'uniq_id' from the id str + the serial no of the inverter + attr['stat_t'] = prfx + row['name'][0] + attr['dev_cla'] = ha['dev_cla'] + attr['stat_cla'] = ha['stat_cla'] + attr['uniq_id'] = ha['id']+snr if 'val_tpl' in ha: - attr['val_tpl'] = ha['val_tpl'] # get value template for complexe data structures + attr['val_tpl'] = ha['val_tpl'] elif 'fmt' in ha: - attr['val_tpl'] = '{{value_json' + f"['{row['name'][-1]}'] {ha['fmt']}" + '}}' # eg. 'val_tpl': "{{ value_json['Output_Power']|float }}" + attr['val_tpl'] = '{{value_json' + f"['{row['name'][-1]}']" + " {ha['fmt']}" + '}}' # eg. 'val_tpl': "{{ value_json['Output_Power']|float }} # noqa: E501 else: self.inc_counter('Internal_Error') - logging.error(f"Infos.__info_defs: the row for {key} do not have a 'val_tpl' nor a 'fmt' value") + logging.error(f"Infos.__info_defs: the row for {key} do" + " not have a 'val_tpl' nor a 'fmt' value") - # add unit_of_meas only, if status_class isn't none. If status_cla is None we want a number format and not line graph in home assistant. - # A unit will change the number format to a line graph - if 'unit' in row and attr['stat_cla'] != None: - attr['unit_of_meas'] = row['unit'] # optional add a 'unit_of_meas' e.g. 'W' + # add unit_of_meas only, if status_class isn't none. If + # status_cla is None we want a number format and not line + # graph in home assistant. A unit will change the number + # format to a line graph + if 'unit' in row and attr['stat_cla'] is not None: + attr['unit_of_meas'] = row['unit'] # 'unit_of_meas' if 'icon' in ha: - attr['ic'] = ha['icon'] # optional add an icon for the entity + attr['ic'] = ha['icon'] # icon for the entity if 'nat_prc' in ha: - attr['sug_dsp_prc'] = ha['nat_prc'] # optional add the precison of floats + attr['sug_dsp_prc'] = ha['nat_prc'] # precison of floats if 'ent_cat' in ha: - attr['ent_cat'] = ha['ent_cat'] # diagnostic, config - - # enabled_by_default is deactivated, since it avoid the via setup of the devices - # it seems, that there is a bug in home assistant. tested with 'Home Assistant 2023.10.4' - #if 'en' in ha: # enabled_by_default + attr['ent_cat'] = ha['ent_cat'] # diagnostic, config + + # enabled_by_default is deactivated, since it avoid the via + # setup of the devices. It seems, that there is a bug in home + # assistant. tested with 'Home Assistant 2023.10.4' + # if 'en' in ha: # enabled_by_default # attr['en'] = ha['en'] - # eg. 'dev':{'name':'Microinverter','mdl':'MS-600','ids':["inverter_123"],'mf':'TSUN','sa': 'auf Garagendach'} - # attr['dev'] = {'name':'Microinverter','mdl':'MS-600','ids':[f'inverter_{snr}'],'mf':'TSUN','sa': 'auf Garagendach'} if 'dev' in ha: device = self.__info_devs[ha['dev']] - if 'dep' in device and self.ignore_this_device(device['dep']): + if 'dep' in device and self.ignore_this_device(device['dep']): # noqa: E501 continue dev = {} - # the same name for 'name' and 'suggested area', so we get dedicated devices in home assistant with short value name and headline - if sug_area == '' or ('singleton' in device and device['singleton']): + # the same name for 'name' and 'suggested area', so we get + # dedicated devices in home assistant with short value + # name and headline + if (sug_area == '' or + ('singleton' in device and device['singleton'])): dev['name'] = device['name'] - dev['sa'] = device['name'] + dev['sa'] = device['name'] else: dev['name'] = device['name']+' - '+sug_area - dev['sa'] = device['name']+' - '+sug_area - - if 'via' in device: # add the link to the parent device + dev['sa'] = device['name']+' - '+sug_area + + if 'via' in device: # add the link to the parent device via = device['via'] if via in self.__info_devs: via_dev = self.__info_devs[via] @@ -245,19 +258,22 @@ class Infos: dev['via_device'] = f"{via}_{snr}" else: self.inc_counter('Internal_Error') - logging.error(f"Infos.__info_defs: the row for {key} has an invalid via value: {via}") + logging.error(f"Infos.__info_defs: the row for " + f"{key} has an invalid via value: " + f"{via}") - - - for key in ('mdl','mf', 'sw', 'hw'): # add optional values fpr 'modell', 'manufaturer', 'sw version' and 'hw version' + for key in ('mdl', 'mf', 'sw', 'hw'): # add optional + # values fpr 'modell', 'manufaturer', 'sw version' and + # 'hw version' if key in device: data = self.dev_value(device[key]) - if data is not None: dev[key] = data - + if data is not None: + dev[key] = data + if 'singleton' in device and device['singleton']: - dev['ids'] = [f"{ha['dev']}"] + dev['ids'] = [f"{ha['dev']}"] else: - dev['ids'] = [f"{ha['dev']}_{snr}"] + dev['ids'] = [f"{ha['dev']}_{snr}"] attr['dev'] = dev @@ -267,35 +283,35 @@ class Infos: attr['o'] = origin else: self.inc_counter('Internal_Error') - logging.error(f"Infos.__info_defs: the row for {key} missing 'dev' value for ha register") + logging.error(f"Infos.__info_defs: the row for {key} " + "missing 'dev' value for ha register") + yield json.dumps(attr), component, node_id, attr['uniq_id'] - yield json.dumps (attr), component, node_id, attr['uniq_id'] - - def inc_counter (self, counter:str) -> None: + def inc_counter(self, counter: str) -> None: '''inc proxy statistic counter''' dict = self.stat['proxy'] - dict[counter] += 1 - - def dec_counter (self, counter:str) -> None: + dict[counter] += 1 + + def dec_counter(self, counter: str) -> None: '''dec proxy statistic counter''' dict = self.stat['proxy'] - dict[counter] -= 1 - - + dict[counter] -= 1 + def __key_obj(self, id) -> list: - d = self.__info_defs.get(id, {'name': None, 'level': logging.DEBUG, 'unit': ''}) + d = self.__info_defs.get(id, {'name': None, 'level': logging.DEBUG, + 'unit': ''}) if 'ha' in d and 'must_incr' in d['ha']: must_incr = d['ha']['must_incr'] else: must_incr = False return d['name'], d['level'], d['unit'], must_incr - - + def parse(self, buf) -> None: - '''parse a data sequence received from the inverter and stores the values in Infos.db - + '''parse a data sequence received from the inverter and + stores the values in Infos.db + buf: buffer of the sequence to parse''' result = struct.unpack_from('!l', buf, 0) elms = result[0] @@ -303,38 +319,39 @@ class Infos: ind = 4 while i < elms: result = struct.unpack_from('!lB', buf, ind) - info_id = result[0] + info_id = result[0] data_type = result[1] ind += 5 keys, level, unit, must_incr = self.__key_obj(info_id) - - if data_type==0x54: # 'T' -> Pascal-String + + if data_type == 0x54: # 'T' -> Pascal-String str_len = buf[ind] - result = struct.unpack_from(f'!{str_len+1}p', buf, ind)[0].decode(encoding='ascii', errors='replace') + result = struct.unpack_from(f'!{str_len+1}p', buf, + ind)[0].decode(encoding='ascii', + errors='replace') ind += str_len+1 - - elif data_type==0x49: # 'I' -> int32 - result = struct.unpack_from(f'!l', buf, ind)[0] + + elif data_type == 0x49: # 'I' -> int32 + result = struct.unpack_from('!l', buf, ind)[0] ind += 4 - elif data_type==0x53: # 'S' -> short - result = struct.unpack_from(f'!h', buf, ind)[0] + elif data_type == 0x53: # 'S' -> short + result = struct.unpack_from('!h', buf, ind)[0] ind += 2 - elif data_type==0x46: # 'F' -> float32 - result = round(struct.unpack_from(f'!f', buf, ind)[0],2) + elif data_type == 0x46: # 'F' -> float32 + result = round(struct.unpack_from('!f', buf, ind)[0], 2) ind += 4 else: self.inc_counter('Invalid_Data_Type') - logging.error(f"Infos.parse: data_type: {data_type} not supported") + logging.error(f"Infos.parse: data_type: {data_type}" + " not supported") return - - if keys: dict = self.db name = '' - + for key in keys[:-1]: if key not in dict: dict[key] = {} @@ -342,22 +359,22 @@ class Infos: name += key + '.' if keys[-1] not in dict: - update = (not must_incr or result>0) + update = (not must_incr or result > 0) else: if must_incr: update = dict[keys[-1]] < result else: update = dict[keys[-1]] != result - - if update: dict[keys[-1]] = result - name += keys[-1] - yield keys[0], update - else: - update = False - name = str(f'info-id.0x{info_id:x}') - - self.tracer.log(level, f'{name} : {result}{unit} update: {update}') - - i +=1 - \ No newline at end of file + if update: + dict[keys[-1]] = result + name += keys[-1] + yield keys[0], update + else: + update = False + name = str(f'info-id.0x{info_id:x}') + + self.tracer.log(level, f'{name} : {result}{unit}' + ' update: {update}') + + i += 1 diff --git a/app/src/inverter.py b/app/src/inverter.py index debfa98..f578202 100644 --- a/app/src/inverter.py +++ b/app/src/inverter.py @@ -1,132 +1,168 @@ -import asyncio, logging, traceback, json +import asyncio +import logging +import traceback +import json from config import Config from async_stream import AsyncStream from mqtt import Mqtt from aiomqtt import MqttCodeError from infos import Infos -#import gc +# import gc -#logger = logging.getLogger('conn') +# logger = logging.getLogger('conn') logger_mqtt = logging.getLogger('mqtt') - class Inverter(AsyncStream): + '''class Inverter is a derivation of an Async_Stream + + The class has some class method for managing common resources like a + connection to the MQTT broker or proxy error counter which are common + for all inverter connection + + Instances of the class are connections to an inverter and can have an + optional link to an remote connection to the TSUN cloud. A remote + connection dies with the inverter connection. + + class methods: + class_init(): initialize the common resources of the proxy (MQTT + broker, Proxy DB, etc). Must be called before the + first Ib´verter instance can be created + class_close(): release the common resources of the proxy. Should not + be called before any instances of the class are + destroyed + + methods: + server_loop(addr): Async loop method for receiving messages from the + inverter (server-side) + client_loop(addr): Async loop method for receiving messages from the + TSUN cloud (client-side) + async_create_remote(): Establish a client connection to the TSUN cloud + async_publ_mqtt(): Publish data to MQTT broker + close(): Release method which must be called before a instance can be + destroyed + ''' @classmethod - def class_init(cls): + def class_init(cls) -> None: logging.debug('Inverter.class_init') # initialize the proxy statistics Infos.static_init() - cls.db_stat = Infos() + cls.db_stat = Infos() ha = Config.get('ha') cls.entity_prfx = ha['entity_prefix'] + '/' cls.discovery_prfx = ha['discovery_prefix'] + '/' - cls.proxy_node_id = ha['proxy_node_id'] + '/' + cls.proxy_node_id = ha['proxy_node_id'] + '/' cls.proxy_unique_id = ha['proxy_unique_id'] # call Mqtt singleton to establisch the connection to the mqtt broker - cls.mqtt = Mqtt(cls.__cb_MqttIsUp) + cls.mqtt = Mqtt(cls.__cb_mqtt_is_up) @classmethod - async def __cb_MqttIsUp(cls): + async def __cb_mqtt_is_up(cls) -> None: logging.info('Initialize proxy device on home assistant') # register proxy status counters at home assistant await cls.__register_proxy_stat_home_assistant() # send values of the proxy status counters - await asyncio.sleep(0.5) # wait a bit, before sending first data - cls.new_stat_data['proxy'] = True # force sending data to sync home assistant + await asyncio.sleep(0.5) # wait a bit, before sending data + cls.new_stat_data['proxy'] = True # force sending data to sync ha await cls.__async_publ_mqtt_proxy_stat('proxy') @classmethod async def __register_proxy_stat_home_assistant(cls) -> None: '''register all our topics at home assistant''' - for data_json, component, node_id, id in cls.db_stat.ha_confs(cls.entity_prfx, cls.proxy_node_id, cls.proxy_unique_id, True): - logger_mqtt.debug(f"MQTT Register: cmp:'{component}' node_id:'{node_id}' {data_json}") - await cls.mqtt.publish(f"{cls.discovery_prfx}{component}/{node_id}{id}/config", data_json) + for data_json, component, node_id, id in cls.db_stat.ha_confs( + cls.entity_prfx, cls.proxy_node_id, + cls.proxy_unique_id, True): + logger_mqtt.debug(f'''MQTT Register: cmp:'{component}' node_id:' +{node_id}' {data_json}''') + await cls.mqtt.publish(f'''{cls.discovery_prfx}{component} +/{node_id}{id}/config''', data_json) @classmethod - async def __async_publ_mqtt_proxy_stat(cls, key): + async def __async_publ_mqtt_proxy_stat(cls, key) -> None: stat = Infos.stat if key in stat and cls.new_stat_data[key]: data_json = json.dumps(stat[key]) node_id = cls.proxy_node_id logger_mqtt.debug(f'{key}: {data_json}') - await cls.mqtt.publish(f"{cls.entity_prfx}{node_id}{key}", data_json) + await cls.mqtt.publish(f"{cls.entity_prfx}{node_id}{key}", + data_json) cls.new_stat_data[key] = False - @classmethod - def class_close(cls, loop): + def class_close(cls, loop) -> None: logging.debug('Inverter.class_close') - logging.info ('Close MQTT Task') + logging.info('Close MQTT Task') loop.run_until_complete(cls.mqtt.close()) cls.mqtt = None - def __init__ (self, reader, writer, addr): + def __init__(self, reader, writer, addr): super().__init__(reader, writer, addr, None, True) self.ha_restarts = -1 - async def server_loop(self, addr): '''Loop for receiving messages from the inverter (server-side)''' - logging.info(f'Accept connection from {addr}') - self.inc_counter ('Inverter_Cnt') + logging.info(f'Accept connection from {addr}') + self.inc_counter('Inverter_Cnt') await self.loop() - self.dec_counter ('Inverter_Cnt') + self.dec_counter('Inverter_Cnt') logging.info(f'Server loop stopped for {addr}') - - # if the server connection closes, we also have to disconnect the connection to te TSUN cloud + + # if the server connection closes, we also have to disconnect + # the connection to te TSUN cloud if self.remoteStream: - logging.debug ("disconnect client connection") + logging.debug("disconnect client connection") self.remoteStream.disc() try: await self.__async_publ_mqtt_proxy_stat('proxy') - except: pass - + except Exception: + pass + async def client_loop(self, addr): '''Loop for receiving messages from the TSUN cloud (client-side)''' - await self.remoteStream.loop() + await self.remoteStream.loop() logging.info(f'Client loop stopped for {addr}') - # if the client connection closes, we don't touch the server connection. Instead we erase the client - # connection stream, thus on the next received packet from the inverter, we can establish a new connection - # to the TSUN cloud - self.remoteStream.remoteStream = None # erase backlink to inverter instance - self.remoteStream = None # than erase client connection - + # if the client connection closes, we don't touch the server + # connection. Instead we erase the client connection stream, + # thus on the next received packet from the inverter, we can + # establish a new connection to the TSUN cloud + self.remoteStream.remoteStream = None # erase backlink to inverter + self.remoteStream = None # than erase client connection + async def async_create_remote(self) -> None: '''Establish a client connection to the TSUN cloud''' tsun = Config.get('tsun') host = tsun['host'] - port = tsun['port'] + port = tsun['port'] addr = (host, port) - + try: logging.info(f'Connected to {addr}') connect = asyncio.open_connection(host, port) - reader, writer = await connect + reader, writer = await connect self.remoteStream = AsyncStream(reader, writer, addr, self, False) asyncio.create_task(self.client_loop(addr)) - + except ConnectionRefusedError as error: logging.info(f'{error}') except Exception: logging.error( f"Inverter: Exception for {addr}:\n" f"{traceback.format_exc()}") - - async def async_publ_mqtt(self) -> None: - '''puplish data to MQTT broker''' - # check if new inverter or collector infos are available or when the home assistant has changed the status back to online + '''publish data to MQTT broker''' + # check if new inverter or collector infos are available or when the + # home assistant has changed the status back to online try: - if (('inverter' in self.new_data and self.new_data['inverter']) or - ('collector' in self.new_data and self.new_data['collector']) or - self.mqtt.ha_restarts != self.ha_restarts): + if (('inverter' in self.new_data and self.new_data['inverter']) + or ('collector' in self.new_data and + self.new_data['collector']) + or self.mqtt.ha_restarts != self.ha_restarts): await self.__register_proxy_stat_home_assistant() await self.__register_home_assistant() self.ha_restarts = self.mqtt.ha_restarts @@ -142,7 +178,6 @@ class Inverter(AsyncStream): logging.error( f"Inverter: Exception:\n" f"{traceback.format_exc()}") - async def __async_publ_mqtt_packet(self, key): db = self.db.db @@ -150,21 +185,25 @@ class Inverter(AsyncStream): data_json = json.dumps(db[key]) node_id = self.node_id logger_mqtt.debug(f'{key}: {data_json}') - await self.mqtt.publish(f"{self.entity_prfx}{node_id}{key}", data_json) + await self.mqtt.publish(f'''{self.entity_prfx}{node_id} +{key}''', data_json) self.new_data[key] = False async def __register_home_assistant(self) -> None: '''register all our topics at home assistant''' - for data_json, component, node_id, id in self.db.ha_confs(self.entity_prfx, self.node_id, self.unique_id, False, self.sug_area): - logger_mqtt.debug(f"MQTT Register: cmp:'{component}' node_id:'{node_id}' {data_json}") - await self.mqtt.publish(f"{self.discovery_prfx}{component}/{node_id}{id}/config", data_json) - + for data_json, component, node_id, id in self.db.ha_confs( + self.entity_prfx, self.node_id, self.unique_id, + False, self.sug_area): + logger_mqtt.debug(f"MQTT Register: cmp:'{component}'" + " node_id:'{node_id}' {data_json}") + await self.mqtt.publish(f"{self.discovery_prfx}{component}" + "/{node_id}{id}/config", data_json) + def close(self) -> None: logging.debug(f'Inverter.close() {self.addr}') super().close() # call close handler in the parent class # logger.debug (f'Inverter refs: {gc.get_referrers(self)}') - - def __del__ (self): - logging.debug ("Inverter.__del__") - super().__del__() + def __del__(self): + logging.debug("Inverter.__del__") + super().__del__() diff --git a/app/src/messages.py b/app/src/messages.py index bd9038d..8e69bf4 100644 --- a/app/src/messages.py +++ b/app/src/messages.py @@ -1,8 +1,10 @@ -import struct, logging, time, datetime +import struct +import logging +import time +import datetime import weakref -from datetime import datetime -if __name__ == "app.src.messages": +if __name__ == "app.src.messages": from app.src.infos import Infos from app.src.config import Config else: # pragma: no cover @@ -10,14 +12,15 @@ else: # pragma: no cover from config import Config logger = logging.getLogger('msg') - - + + def hex_dump_memory(level, info, data, num): n = 0 lines = [] lines.append(info) tracer = logging.getLogger('tracer') - if not tracer.isEnabledFor(level): return + if not tracer.isEnabledFor(level): + return for i in range(0, num, 16): line = ' ' @@ -25,48 +28,52 @@ def hex_dump_memory(level, info, data, num): n += 16 for j in range(n-16, n): - if j >= len(data): break + if j >= len(data): + break line += '%02x ' % abs(data[j]) line += ' ' * (3 * 16 + 9 - len(line)) + ' | ' for j in range(n-16, n): - if j >= len(data): break + if j >= len(data): + break c = data[j] if not (data[j] < 0x20 or data[j] > 0x7e) else '.' line += '%c' % c lines.append(line) tracer.log(level, '\n'.join(lines)) - + class Control: - def __init__(self, ctrl:int): + def __init__(self, ctrl: int): self.ctrl = ctrl - + def __int__(self) -> int: return self.ctrl def is_ind(self) -> bool: return (self.ctrl == 0x91) - - #def is_req(self) -> bool: + + # def is_req(self) -> bool: # return not (self.ctrl & 0x08) - + def is_resp(self) -> bool: return (self.ctrl == 0x99) + class IterRegistry(type): def __iter__(cls): for ref in cls._registry: obj = ref() - if obj is not None: yield obj + if obj is not None: + yield obj class Message(metaclass=IterRegistry): _registry = [] new_stat_data = {} - + def __init__(self): self._registry.append(weakref.ref(self)) self.header_valid = False @@ -78,19 +85,21 @@ class Message(metaclass=IterRegistry): self._recv_buffer = b'' self._send_buffer = bytearray(0) self._forward_buffer = bytearray(0) - self.db = Infos() + self.db = Infos() self.new_data = {} - self.switch={ + self.switch = { 0x00: self.msg_contact_info, 0x22: self.msg_get_time, 0x71: self.msg_collector_data, 0x04: self.msg_inverter_data, } - + ''' - Empty methods, that have to be implemented in any child class which don't use asyncio + Empty methods, that have to be implemented in any child class which + don't use asyncio ''' - def _read(self) -> None: # read data bytes from socket and copy them to our _recv_buffer + def _read(self) -> None: # read data bytes from socket and copy them + # to our _recv_buffer return # pragma: no cover ''' @@ -98,96 +107,104 @@ class Message(metaclass=IterRegistry): ''' def close(self) -> None: # we have refernces to methods of this class in self.switch - # so we have to erase self.switch, otherwise this instance can't be + # so we have to erase self.switch, otherwise this instance can't be # deallocated by the garbage collector ==> we get a memory leak - del self.switch + del self.switch - def inc_counter(self, counter:str) -> None: + def inc_counter(self, counter: str) -> None: self.db.inc_counter(counter) self.new_stat_data['proxy'] = True - def dec_counter(self, counter:str) -> None: + def dec_counter(self, counter: str) -> None: self.db.dec_counter(counter) self.new_stat_data['proxy'] = True - - def set_serial_no(self, serial_no : str): - - if self.unique_id == serial_no: + + def set_serial_no(self, serial_no: str): + + if self.unique_id == serial_no: logger.debug(f'SerialNo: {serial_no}') else: inverters = Config.get('inverters') - #logger.debug(f'Inverters: {inverters}') - + # logger.debug(f'Inverters: {inverters}') + if serial_no in inverters: inv = inverters[serial_no] self.node_id = inv['node_id'] - self.sug_area = inv['suggested_area'] - logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') - else: + self.sug_area = inv['suggested_area'] + logger.debug(f'''SerialNo {serial_no} allowed! + area:{self.sug_area}''') + else: self.node_id = '' - self.sug_area = '' + self.sug_area = '' if 'allow_all' not in inverters or not inverters['allow_all']: self.inc_counter('Unknown_SNR') self.unique_id = None - logger.warning(f'ignore message from unknow inverter! (SerialNo: {serial_no})') + logger.warning(f'''ignore message from unknow inverter! + (SerialNo: {serial_no})''') return logger.debug(f'SerialNo {serial_no} not known but accepted!') - + self.unique_id = serial_no - def read(self) -> None: self._read() - + if not self.header_valid: self.__parse_header(self._recv_buffer, len(self._recv_buffer)) - - if self.header_valid and len(self._recv_buffer) >= (self.header_len+self.data_len): - hex_dump_memory(logging.INFO, f'Received from {self.addr}:', self._recv_buffer, self.header_len+self.data_len) - self.set_serial_no(self.id_str.decode("utf-8")) + if self.header_valid and len(self._recv_buffer) >= (self.header_len + + self.data_len): + hex_dump_memory(logging.INFO, f'Received from {self.addr}:', + self._recv_buffer, self.header_len+self.data_len) + + self.set_serial_no(self.id_str.decode("utf-8")) self.__dispatch_msg() self.__flush_recv_msg() return - + def forward(self, buffer, buflen) -> None: tsun = Config.get('tsun') if tsun['enabled']: self._forward_buffer = buffer[:buflen] - hex_dump_memory(logging.DEBUG, 'Store for forwarding:', buffer, buflen) + hex_dump_memory(logging.DEBUG, 'Store for forwarding:', + buffer, buflen) - self.__parse_header(self._forward_buffer, len(self._forward_buffer)) + self.__parse_header(self._forward_buffer, + len(self._forward_buffer)) fnc = self.switch.get(self.msg_id, self.msg_unknown) - logger.info(self.__flow_str(self.server_side, 'forwrd') + f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}' ) + logger.info(self.__flow_str(self.server_side, 'forwrd') + + f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}') return - + ''' Our private methods ''' - def __flow_str(self, server_side:bool, type:('rx','tx','forwrd', 'drop')): - switch={ - 'rx': ' <', - 'tx': ' >', - 'forwrd': '<< ', - 'drop': ' xx', - 'rxS': '> ', - 'txS': '< ', - 'forwrdS':' >>', - 'dropS': 'xx ', + def __flow_str(self, server_side: bool, type: + ('rx', 'tx', 'forwrd', 'drop')): # noqa: F821 + switch = { + 'rx': ' <', + 'tx': ' >', + 'forwrd': '<< ', + 'drop': ' xx', + 'rxS': '> ', + 'txS': '< ', + 'forwrdS': ' >>', + 'dropS': 'xx ', } - if server_side: type +='S' + if server_side: + type += 'S' return switch.get(type, '???') def __timestamp(self): if False: - # utc as epoche + # utc as epoche ts = time.time() else: - # convert localtime in epoche - ts = (datetime.now() - datetime(1970,1,1)).total_seconds() - return round(ts*1000) + # convert localtime in epoche + ts = (datetime.now() - datetime(1970, 1, 1)).total_seconds() + return round(ts*1000) - # check if there is a complete header in the buffer, parse it + # check if there is a complete header in the buffer, parse it # and set # self.header_len # self.data_len @@ -196,52 +213,53 @@ class Message(metaclass=IterRegistry): # self.msg_id # # if the header is incomplete, than self.header_len is still 0 - # - def __parse_header(self, buf:bytes, buf_len:int) -> None: - - if (buf_len <5): # enough bytes to read len and id_len? + # + def __parse_header(self, buf: bytes, buf_len: int) -> None: + + if (buf_len < 5): # enough bytes to read len and id_len? return result = struct.unpack_from('!lB', buf, 0) - len = result[0] # len of complete message + len = result[0] # len of complete message id_len = result[1] # len of variable id string hdr_len = 5+id_len+2 - + if (buf_len < hdr_len): # enough bytes for complete header? return - + result = struct.unpack_from(f'!{id_len+1}pBB', buf, 4) - - # store parsed header values in the class + + # store parsed header values in the class self.id_str = result[0] - self.ctrl = Control(result[1]) - self.msg_id = result[2] + self.ctrl = Control(result[1]) + self.msg_id = result[2] self.data_len = len-id_len-3 self.header_len = hdr_len self.header_valid = True return - + def __build_header(self, ctrl) -> None: - self.send_msg_ofs = len (self._send_buffer) - self._send_buffer += struct.pack(f'!l{len(self.id_str)+1}pBB', 0, self.id_str, ctrl, self.msg_id) + self.send_msg_ofs = len(self._send_buffer) + self._send_buffer += struct.pack(f'!l{len(self.id_str)+1}pBB', + 0, self.id_str, ctrl, self.msg_id) fnc = self.switch.get(self.msg_id, self.msg_unknown) - logger.info(self.__flow_str(self.server_side, 'tx') + f' Ctl: {int(ctrl):#02x} Msg: {fnc.__name__!r}' ) - + logger.info(self.__flow_str(self.server_side, 'tx') + + f' Ctl: {int(ctrl):#02x} Msg: {fnc.__name__!r}') + def __finish_send_msg(self) -> None: _len = len(self._send_buffer) - self.send_msg_ofs - struct.pack_into('!l',self._send_buffer, self.send_msg_ofs, _len-4) - - + struct.pack_into('!l', self._send_buffer, self.send_msg_ofs, _len-4) def __dispatch_msg(self) -> None: fnc = self.switch.get(self.msg_id, self.msg_unknown) - if self.unique_id: - logger.info(self.__flow_str(self.server_side, 'rx') + f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}' ) + if self.unique_id: + logger.info(self.__flow_str(self.server_side, 'rx') + + f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}') fnc() else: - logger.info(self.__flow_str(self.server_side, 'drop') + f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}' ) - - + logger.info(self.__flow_str(self.server_side, 'drop') + + f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}') + def __flush_recv_msg(self) -> None: self._recv_buffer = self._recv_buffer[(self.header_len+self.data_len):] self.header_valid = False @@ -255,7 +273,7 @@ class Message(metaclass=IterRegistry): self._send_buffer += b'\x01' self.__finish_send_msg() elif self.ctrl.is_resp(): - return # ignore received response from tsun + return # ignore received response from tsun else: self.inc_counter('Unknown_Ctrl') @@ -265,38 +283,37 @@ class Message(metaclass=IterRegistry): if self.ctrl.is_ind(): ts = self.__timestamp() logger.debug(f'time: {ts:08x}') - + self.__build_header(0x99) self._send_buffer += struct.pack('!q', ts) self.__finish_send_msg() - + elif self.ctrl.is_resp(): - result = struct.unpack_from(f'!q', self._recv_buffer, self.header_len) + result = struct.unpack_from('!q', self._recv_buffer, + self.header_len) logger.debug(f'tsun-time: {result[0]:08x}') - return # ignore received response from tsun + return # ignore received response from tsun else: self.inc_counter('Unknown_Ctrl') - self.forward(self._recv_buffer, self.header_len+self.data_len) - - def parse_msg_header(self): result = struct.unpack_from('!lB', self._recv_buffer, self.header_len) data_id = result[0] # len of complete message - id_len = result[1] # len of variable id string + id_len = result[1] # len of variable id string logger.debug(f'Data_ID: {data_id} id_len: {id_len}') - - msg_hdr_len= 5+id_len+9 - - result = struct.unpack_from(f'!{id_len+1}pBq', self._recv_buffer, self.header_len+4) - + + msg_hdr_len = 5+id_len+9 + + result = struct.unpack_from(f'!{id_len+1}pBq', self._recv_buffer, + self.header_len + 4) + logger.debug(f'ID: {result[0]} B: {result[1]}') logger.debug(f'time: {result[2]:08x}') - #logger.info(f'time: {datetime.utcfromtimestamp(result[2]).strftime("%Y-%m-%d %H:%M:%S")}') + # logger.info(f'time: {datetime.utcfromtimestamp(result[2]).strftime( + # "%Y-%m-%d %H:%M:%S")}') return msg_hdr_len - def msg_collector_data(self): if self.ctrl.is_ind(): @@ -304,14 +321,13 @@ class Message(metaclass=IterRegistry): self._send_buffer += b'\x01' self.__finish_send_msg() self.__process_data() - + elif self.ctrl.is_resp(): - return # ignore received response + return # ignore received response else: self.inc_counter('Unknown_Ctrl') - - self.forward(self._recv_buffer, self.header_len+self.data_len) + self.forward(self._recv_buffer, self.header_len+self.data_len) def msg_inverter_data(self): if self.ctrl.is_ind(): @@ -319,27 +335,23 @@ class Message(metaclass=IterRegistry): self._send_buffer += b'\x01' self.__finish_send_msg() self.__process_data() - + elif self.ctrl.is_resp(): - return # ignore received response + return # ignore received response else: self.inc_counter('Unknown_Ctrl') - + self.forward(self._recv_buffer, self.header_len+self.data_len) - + def __process_data(self): msg_hdr_len = self.parse_msg_header() - for key, update in self.db.parse(self._recv_buffer[self.header_len + msg_hdr_len:]): - if update: self.new_data[key] = True - - + for key, update in self.db.parse(self._recv_buffer[self.header_len + + msg_hdr_len:]): + if update: + self.new_data[key] = True def msg_unknown(self): - logger.warning (f"Unknow Msg: ID:{self.msg_id}") + logger.warning(f"Unknow Msg: ID:{self.msg_id}") self.inc_counter('Unknown_Msg') self.forward(self._recv_buffer, self.header_len+self.data_len) - - - - diff --git a/app/src/mqtt.py b/app/src/mqtt.py index 11f3423..f8a6133 100644 --- a/app/src/mqtt.py +++ b/app/src/mqtt.py @@ -1,4 +1,5 @@ -import asyncio, logging +import asyncio +import logging import aiomqtt from config import Config @@ -7,10 +8,12 @@ logger_mqtt = logging.getLogger('mqtt') class Singleton(type): _instances = {} + def __call__(cls, *args, **kwargs): - logger_mqtt.debug(f'singleton: __call__') + logger_mqtt.debug('singleton: __call__') if cls not in cls._instances: - cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + cls._instances[cls] = super(Singleton, + cls).__call__(*args, **kwargs) return cls._instances[cls] @@ -19,68 +22,72 @@ class Mqtt(metaclass=Singleton): cb_MqttIsUp = None def __init__(self, cb_MqttIsUp): - logger_mqtt.debug(f'MQTT: __init__') - if cb_MqttIsUp: self.cb_MqttIsUp = cb_MqttIsUp + logger_mqtt.debug('MQTT: __init__') + if cb_MqttIsUp: + self.cb_MqttIsUp = cb_MqttIsUp loop = asyncio.get_event_loop() self.task = loop.create_task(self.__loop()) - self.ha_restarts = 0 + self.ha_restarts = 0 - @property def ha_restarts(self): return self._ha_restarts - + @ha_restarts.setter def ha_restarts(self, value): self._ha_restarts = value - + def __del__(self): - logger_mqtt.debug(f'MQTT: __del__') - + logger_mqtt.debug('MQTT: __del__') async def close(self) -> None: - logger_mqtt.debug(f'MQTT: close') + logger_mqtt.debug('MQTT: close') self.task.cancel() try: await self.task except Exception as e: logging.debug(f"Mqtt.close: exception: {e} ...") - - - async def publish(self, topic: str, payload: str | bytes | bytearray | int | float | None = None) -> None: + async def publish(self, topic: str, payload: str | bytes | bytearray + | int | float | None = None) -> None: if self.client: await self.client.publish(topic, payload) - async def __loop(self) -> None: mqtt = Config.get('mqtt') ha = Config.get('ha') - logger_mqtt.info(f'start MQTT: host:{mqtt["host"]} port:{mqtt["port"]} user:{mqtt["user"]}') - self.client = aiomqtt.Client(hostname=mqtt['host'], port=mqtt['port'], username=mqtt['user'], password=mqtt['passwd']) - + logger_mqtt.info(f'start MQTT: host:{mqtt["host"]} port:' + '{mqtt["port"]} ' + 'user:{mqtt["user"]}') + self.client = aiomqtt.Client(hostname=mqtt['host'], port=mqtt['port'], + username=mqtt['user'], + password=mqtt['passwd']) + interval = 5 # Seconds while True: try: async with self.client: logger_mqtt.info('MQTT broker connection established') - if self.cb_MqttIsUp: + if self.cb_MqttIsUp: await self.cb_MqttIsUp() - + async with self.client.messages() as messages: - await self.client.subscribe(f"{ha['auto_conf_prefix']}/status") + await self.client.subscribe(f"{ha['auto_conf_prefix']}" + "/status") async for message in messages: status = message.payload.decode("UTF-8") - logger_mqtt.info(f'Home-Assistant Status: {status}') + logger_mqtt.info('Home-Assistant Status:' + f' {status}') if status == 'online': self.ha_restarts += 1 await self.cb_MqttIsUp() except aiomqtt.MqttError: - logger_mqtt.info(f"Connection lost; Reconnecting in {interval} seconds ...") + logger_mqtt.info(f"Connection lost; Reconnecting in {interval}" + " seconds ...") await asyncio.sleep(interval) except asyncio.CancelledError: - logger_mqtt.debug(f"MQTT task cancelled") + logger_mqtt.debug("MQTT task cancelled") self.client = None return diff --git a/app/src/server.py b/app/src/server.py index 6d246cb..7375244 100644 --- a/app/src/server.py +++ b/app/src/server.py @@ -1,15 +1,19 @@ -import logging, asyncio, signal, functools, os -from logging import config +import logging +import asyncio +import signal +import functools +import os +# from logging import config from async_stream import AsyncStream from inverter import Inverter from config import Config - + async def handle_client(reader, writer): '''Handles a new incoming connection and starts an async loop''' addr = writer.get_extra_info('peername') - await Inverter(reader, writer, addr).server_loop(addr) + await Inverter(reader, writer, addr).server_loop(addr) def handle_SIGTERM(loop): @@ -29,14 +33,15 @@ def handle_SIGTERM(loop): loop.stop() logging.info('Shutdown complete') - + def get_log_level() -> int: - '''checks if LOG_LVL is set in the environment and returns the corresponding logging.LOG_LEVEL''' + '''checks if LOG_LVL is set in the environment and returns the + corresponding logging.LOG_LEVEL''' log_level = os.getenv('LOG_LVL', 'INFO') - if log_level== 'DEBUG': + if log_level == 'DEBUG': log_level = logging.DEBUG - elif log_level== 'WARN': + elif log_level == 'WARN': log_level = logging.WARNING else: log_level = logging.INFO @@ -48,44 +53,46 @@ if __name__ == "__main__": # Setup our daily, rotating logger # serv_name = os.getenv('SERVICE_NAME', 'proxy') - version = os.getenv('VERSION', 'unknown') - + version = os.getenv('VERSION', 'unknown') + logging.config.fileConfig('logging.ini') logging.info(f'Server "{serv_name} - {version}" will be started') - + # set lowest-severity for 'root', 'msg', 'conn' and 'data' logger log_level = get_log_level() logging.getLogger().setLevel(log_level) logging.getLogger('msg').setLevel(log_level) logging.getLogger('conn').setLevel(log_level) logging.getLogger('data').setLevel(log_level) - + # read config file - Config.read() + Config.read() loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) Inverter.class_init() # - # Register some UNIX Signal handler for a gracefully server shutdown on Docker restart and stop - # - for signame in ('SIGINT','SIGTERM'): - loop.add_signal_handler(getattr(signal, signame), functools.partial(handle_SIGTERM, loop)) + # Register some UNIX Signal handler for a gracefully server shutdown + # on Docker restart and stop + # + for signame in ('SIGINT', 'SIGTERM'): + loop.add_signal_handler(getattr(signal, signame), + functools.partial(handle_SIGTERM, loop)) # - # Create a task for our listening server. This must be a task! If we call start_server directly out - # of our main task, the eventloop will be blocked and we can't receive and handle the UNIX signals! - # + # Create a task for our listening server. This must be a task! If we call + # start_server directly out of our main task, the eventloop will be blocked + # and we can't receive and handle the UNIX signals! + # loop.create_task(asyncio.start_server(handle_client, '0.0.0.0', 5005)) - + try: loop.run_forever() except KeyboardInterrupt: pass finally: Inverter.class_close(loop) - logging.info ('Close event loop') + logging.info('Close event loop') loop.close() - logging.info (f'Finally, exit Server "{serv_name}"') - \ No newline at end of file + logging.info(f'Finally, exit Server "{serv_name}"')