diff --git a/.gitignore b/.gitignore index 07bcca2..1989277 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,5 @@ __pycache__ mosquitto/** homeassistant/** tsun_proxy/** -system_tests/** Doku/** .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 9478712..e2b65c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - fix issue [#8](https://github.com/s-allius/tsun-gen3-proxy/issues/8) +- implement [#10](https://github.com/s-allius/tsun-gen3-proxy/issues/10) +- fix: don't dispatch ignored messages so that they are not forwarded +- add systemtests +- fix unit tests, which were broken since version 0.3.0 +- add proxy device to home assistant +- add statistic counter to proxy device +- support multiple inverter registration at home assistant ## [0.3.0] - 2023-10-10 diff --git a/app/config/default_config.toml b/app/config/default_config.toml index 9f26f1b..6aa1119 100644 --- a/app/config/default_config.toml +++ b/app/config/default_config.toml @@ -15,6 +15,8 @@ mqtt.passwd = '' ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates ha.discovery_prefix = 'homeassistant' # MQTT prefix for discovery topic ha.entity_prefix = 'tsun' # MQTT topic prefix for publishing inverter values +ha.proxy_node_id = 'proxy' # MQTT node id, for the proxy_node_id +ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a proxy instance # microinverters inverters.allow_all = true # allow inverters, even if we have no inverter mapping diff --git a/app/src/async_stream.py b/app/src/async_stream.py index 921758a..eddf568 100644 --- a/app/src/async_stream.py +++ b/app/src/async_stream.py @@ -14,50 +14,16 @@ class AsyncStream(Message): self.remoteStream = remote_stream self.server_side = server_side self.addr = addr - self.unique_id = 0 - self.node_id = '' ''' Our puplic methods ''' - def set_serial_no(self, serial_no : str): - logger.debug(f'SerialNo: {serial_no}') - - if self.unique_id != serial_no: - - inverters = Config.get('inverters') - #logger.debug(f'Inverters: {inverters}') - - if serial_no in inverters: - logger.debug(f'SerialNo {serial_no} allowed!') - inv = inverters[serial_no] - self.node_id = inv['node_id'] - self.sug_area = inv['suggested_area'] - else: - logger.debug(f'SerialNo {serial_no} not known!') - self.node_id = '' - self.sug_area = '' - if not inverters['allow_all']: - self.unique_id = None - - logger.warning(f'ignore message from unknow inverter! (SerialNo: {serial_no})') - return - - self.unique_id = serial_no - - - - - async def loop(self) -> None: while True: try: await self.__async_read() - - if self.id_str: - self.set_serial_no(self.id_str.decode("utf-8")) - + if self.unique_id: await self.__async_write() await self.__async_forward() @@ -114,7 +80,7 @@ class AsyncStream(Message): await self.async_create_remote() # only implmeneted for server side => syncServerStream if self.remoteStream: - hex_dump_memory(logging.DEBUG, f'Forward to {self.remoteStream.addr}:', self._forward_buffer, len(self._forward_buffer)) + 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) diff --git a/app/src/config.py b/app/src/config.py index 76a1fa1..8a4fc00 100644 --- a/app/src/config.py +++ b/app/src/config.py @@ -25,7 +25,9 @@ class Config(): 'ha': { 'auto_conf_prefix': Use(str), 'discovery_prefix': Use(str), - 'entity_prefix': Use(str)}, + 'entity_prefix': Use(str), + 'proxy_node_id': Use(str), + 'proxy_unique_id': Use(str)}, 'inverters': { 'allow_all' : Use(bool), diff --git a/app/src/infos.py b/app/src/infos.py index 69afcdd..c179614 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -3,14 +3,19 @@ import struct, json, logging, os class Infos: + stat = {} def __init__(self): self.db = {} self.app_name = os.getenv('SERVICE_NAME', 'proxy') self.version = os.getenv('VERSION', 'unknown') self.tracer = logging.getLogger('data') + prxy = self.__info_devs['proxy'] + prxy['sw'] = self.version + prxy['mdl'] = self.app_name __info_devs={ - 'controller':{ 'name':'Controller', 'mdl':0x00092f90, 'mf':0x000927c0, 'sw':0x00092ba8}, + '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}}, @@ -33,7 +38,13 @@ class Infos: 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': ''}, - + + # proxy: + 0xffffff00: {'name':['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha':{'dev':'proxy', 'dev_cla': None, 'stat_cla': None, 'id':'inv_count_', 'fmt':'| int', 'name': 'Inverter Connection Count', 'icon':'mdi:counter'}}, + 0xffffff01: {'name':['proxy', 'Unknown_SNR'], 'singleton': True, 'ha':{'dev':'proxy', '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', 'dev_cla': None, 'stat_cla': None, 'id':'unknown_msg_', 'fmt':'| int', 'name': 'Unknown Msg 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'}}, + # events 0x00000191: {'name':['events', '401_'], 'level': logging.DEBUG, 'unit': ''}, 0x00000192: {'name':['events', '402_'], 'level': logging.DEBUG, 'unit': ''}, @@ -53,26 +64,26 @@ class Infos: 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'}}, - 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'}}, - 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'}}, + 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': 'power', 'stat_cla': 'measurement', 'id':'rated_power_','fmt':'| int', 'name': 'Rated Power','ent_cat':'diagnostic'}}, + 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'}}, # 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_', 'name': 'Voltage', 'val_tpl' :"{{ (value_json['pv1']['Voltage'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}}, - 0x00000708: {'name':['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv1_', 'name': 'Current', 'val_tpl' :"{{ (value_json['pv1']['Current'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}}, - 0x0000076c: {'name':['input', 'pv1', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha':{'dev':'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv1_','name': 'Power', 'val_tpl' :"{{ (value_json['pv1']['Power'] | float)}}", 'icon':'mdi:gauge'}}, - 0x000007d0: {'name':['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv2_', 'name': 'Voltage', 'val_tpl' :"{{ (value_json['pv2']['Voltage'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}}, - 0x00000834: {'name':['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv2_', 'name': 'Current', 'val_tpl' :"{{ (value_json['pv2']['Current'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}}, - 0x00000898: {'name':['input', 'pv2', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha':{'dev':'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv2_','name': 'Power', 'val_tpl' :"{{ (value_json['pv2']['Power'] | float)}}", 'icon':'mdi:gauge'}}, - 0x000008fc: {'name':['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv3_', 'name': 'Voltage', 'val_tpl' :"{{ (value_json['pv3']['Voltage'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}}, - 0x00000960: {'name':['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv3_', 'name': 'Current', 'val_tpl' :"{{ (value_json['pv3']['Current'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}}, - 0x000009c4: {'name':['input', 'pv3', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha':{'dev':'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv3_','name': 'Power', 'val_tpl' :"{{ (value_json['pv3']['Power'] | float)}}", 'icon':'mdi:gauge'}}, - 0x00000a28: {'name':['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv4_', 'name': 'Voltage', 'val_tpl' :"{{ (value_json['pv4']['Voltage'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}}, - 0x00000a8c: {'name':['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv4_', 'name': 'Current', 'val_tpl' :"{{ (value_json['pv4']['Current'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}}, - 0x00000af0: {'name':['input', 'pv4', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha':{'dev':'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv4_','name': 'Power', 'val_tpl' :"{{ (value_json['pv4']['Power'] | float)}}", 'icon':'mdi:gauge'}}, + 0x000006a4: {'name':['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv1_', 'name': 'Voltage', '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_', 'name': 'Current', '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_','name': 'Power', '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_', 'name': 'Voltage', '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_', 'name': 'Current', '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_','name': 'Power', '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_', 'name': 'Voltage', '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_', 'name': 'Current', '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_','name': 'Power', '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_', 'name': 'Voltage', '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_', 'name': 'Current', '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_','name': 'Power', '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'}}, 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'}}, 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'}}, @@ -86,7 +97,7 @@ class Infos: 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'}}, # 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','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'}}, 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'}}, @@ -127,7 +138,7 @@ class Infos: return not value <= dep['less_eq'] return True - def ha_confs(self, prfx="tsun/garagendach/", snr='123', sug_area =''): + def ha_confs(self, ha_prfx, inv_node_id, inv_snr, proxy_node_id, proxy_unique_id, sug_area =''): '''Generator function yields a json register struct for home-assistant auto configuration and a unique entity string arguments: @@ -137,6 +148,13 @@ class Infos: tab = self.__info_defs for key in tab: row = tab[key] + if 'singleton' in row and row['singleton']: + node_id = proxy_node_id + snr = proxy_unique_id + else: + node_id = inv_node_id + snr = inv_snr + prfx = ha_prfx + node_id #check if we have details for home assistant if 'ha' in row: @@ -160,7 +178,9 @@ class Infos: elif 'fmt' in ha: attr['val_tpl'] = '{{value_json' + f"['{row['name'][-1]}'] {ha['fmt']}" + '}}' # eg. 'val_tpl': "{{ value_json['Output_Power']|float }}" - if 'unit' in row: + # 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' if 'icon' in ha: attr['ic'] = ha['icon'] # optional add an icon for the entity @@ -179,24 +199,34 @@ class Infos: dev = {} - # the same name fpr 'name' and 'suggested area', so we get dedicated devices in home assistant with short value name and headline - if 'name' in device: + # 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'] - # fixme: we ignore the suggested area, since one area make no sense for multiple devices - #else: - # dev['name'] = sug_area - # dev['sa'] = sug_area - + 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['via_device'] = f"{device['via']}_{snr}" + via = device['via'] + if via in self.__info_devs: + via_dev = self.__info_devs[via] + if 'singleton' in via_dev and via_dev['singleton']: + dev['via_device'] = via + else: + dev['via_device'] = f"{via}_{snr}" + 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 'singleton' in device and device['singleton']: + dev['ids'] = [f"{ha['dev']}"] + else: + dev['ids'] = [f"{ha['dev']}_{snr}"] - dev['ids'] = [f"{ha['dev']}_{snr}"] attr['dev'] = dev origin = {} @@ -205,11 +235,26 @@ class Infos: attr['o'] = origin - yield json.dumps (attr), component, attr['uniq_id'] + yield json.dumps (attr), component, node_id, attr['uniq_id'] + def __init_counter (self, counter:str) -> dict: + '''init proxy statistic counter, when its missing''' + if not 'proxy' in self.stat: + self.stat['proxy'] = {} + dict = self.stat['proxy'] + if not counter in dict: + dict[counter] = 0 + return dict - - + def inc_counter (self, counter:str) -> None: + '''inc proxy statistic counter''' + dict = self.__init_counter (counter) + dict[counter] += 1 + + def dec_counter (self, counter:str) -> None: + '''dec proxy statistic counter''' + dict = self.__init_counter (counter) + dict[counter] -= 1 def __key_obj(self, id) -> list: diff --git a/app/src/inverter.py b/app/src/inverter.py index cba3d8b..f9f70f2 100644 --- a/app/src/inverter.py +++ b/app/src/inverter.py @@ -14,22 +14,28 @@ class Inverter(AsyncStream): def __init__ (self, reader, writer, addr): super().__init__(reader, writer, addr, None, True) self.mqtt = Mqtt() - self.ha_restarts = 0 + self.ha_restarts = -1 ha = Config.get('ha') - self.entitiy_prfx = ha['entity_prefix'] + '/' + self.entity_prfx = ha['entity_prefix'] + '/' self.discovery_prfx = ha['discovery_prefix'] + '/' + self.proxy_node_id = ha['proxy_node_id'] + '/' + self.proxy_unique_id = ha['proxy_unique_id'] 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') await self.loop() + 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 self.remoteStream: logging.debug ("disconnect client connection") self.remoteStream.disc() + + # await self.async_publ_mqtt() async def client_loop(self, addr): '''Loop for receiving messages from the TSUN cloud (client-side)''' @@ -68,6 +74,7 @@ class Inverter(AsyncStream): async def async_publ_mqtt(self) -> None: '''puplish data to MQTT broker''' db = self.db.db + stat = self.db.stat # check if new inverter or collector infos are available or when the home assistant has changed the status back to online if (('inverter' in self.new_data and self.new_data['inverter']) or ('collector' in self.new_data and self.new_data['collector']) or @@ -76,18 +83,25 @@ class Inverter(AsyncStream): self.ha_restarts = self.mqtt.ha_restarts for key in self.new_data: - if self.new_data[key] and key in db: - data_json = json.dumps(db[key]) + if self.new_data[key]: + if key in db: + data_json = json.dumps(db[key]) + node_id = self.node_id + elif key in stat: + data_json = json.dumps(stat[key]) + node_id = self.proxy_node_id + else: + continue logger_mqtt.debug(f'{key}: {data_json}') - await self.mqtt.publish(f"{self.entitiy_prfx}{self.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''' try: - for data_json, component, id in self.db.ha_confs(self.entitiy_prfx + self.node_id, self.unique_id, self.sug_area): - logger_mqtt.debug(f'MQTT Register: {data_json}') - await self.mqtt.publish(f"{self.discovery_prfx}{component}/{self.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, self.proxy_node_id, self.proxy_unique_id, 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) except Exception: logging.error( f"Inverter: Exception:\n" diff --git a/app/src/messages.py b/app/src/messages.py index aaa668c..e55af94 100644 --- a/app/src/messages.py +++ b/app/src/messages.py @@ -80,6 +80,9 @@ class Message(metaclass=IterRegistry): self.header_valid = False self.header_len = 0 self.data_len = 0 + self.unique_id = 0 + self.node_id = '' + self.sug_area = '' self._recv_buffer = b'' self._send_buffer = bytearray(0) self._forward_buffer = bytearray(0) @@ -106,7 +109,40 @@ class Message(metaclass=IterRegistry): # 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 + + def inc_counter(self, counter:str) -> None: + self.db.inc_counter(counter) + self.new_data['proxy'] = True + + def dec_counter(self, counter:str) -> None: + self.db.dec_counter(counter) + self.new_data['proxy'] = True + + 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}') + + 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.node_id = '' + 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})') + return + logger.debug(f'SerialNo {serial_no} not known but accepted!') + + self.unique_id = serial_no + def read(self) -> None: self._read() @@ -115,6 +151,11 @@ class Message(metaclass=IterRegistry): 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) + + if self.id_str: + self.set_serial_no(self.id_str.decode("utf-8")) + self.__dispatch_msg() self.__flush_recv_msg() return @@ -203,11 +244,12 @@ class Message(metaclass=IterRegistry): def __dispatch_msg(self) -> None: - hex_dump_memory(logging.INFO, f'Received from {self.addr}:', self._recv_buffer, self.header_len+self.data_len) - fnc = self.switch.get(self.msg_id, self.msg_unknown) - logger.info(self.__flow_str(self.server_side, 'rx') + f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}' ) - fnc() + 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}' ) def __flush_recv_msg(self) -> None: @@ -296,6 +338,7 @@ class Message(metaclass=IterRegistry): def msg_unknown(self): 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/tests/test_infos.py b/app/tests/test_infos.py index ac5a99c..138d627 100644 --- a/app/tests/test_infos.py +++ b/app/tests/test_infos.py @@ -25,7 +25,7 @@ def test_parse_control(ContrDataSeq): pass assert json.dumps(i.db) == json.dumps( -{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Signal_Strength": 100, "Power_On_Time": 29, "Data_Up_Interval": 300}}) +{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com", "No_Inputs": 2}, "controller": {"Signal_Strength": 100, "Power_On_Time": 29, "Data_Up_Interval": 300}}) def test_parse_inverter(InvDataSeq): i = Infos() @@ -45,14 +45,14 @@ def test_parse_cont_and_invert(ContrDataSeq, InvDataSeq): assert json.dumps(i.db) == json.dumps( { -"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Signal_Strength": 100, "Power_On_Time": 29, "Data_Up_Interval": 300}, +"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com", "No_Inputs": 2}, "controller": {"Signal_Strength": 100, "Power_On_Time": 29, "Data_Up_Interval": 300}, "inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T17E7307021D006A", "Equipment_Model": "TSOL-MS600"}}) def test_build_ha_conf1(ContrDataSeq): i = Infos() tests = 0 - for d_json, comp, id in i.ha_confs(prfx="tsun/garagendach/", snr='123'): + for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", inv_snr='123', inv_node_id="garagendach/",proxy_node_id = 'proxy/', proxy_unique_id = '456'): if id == 'out_power_123': assert comp == 'sensor' @@ -66,19 +66,18 @@ def test_build_ha_conf1(ContrDataSeq): elif id == 'power_pv1_123': assert comp == 'sensor' - assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv1_123", "val_tpl": "{{ (value_json['pv1']['Power'] | float)}}", "unit_of_meas": "W", "ic": "mdi:gauge", "dev": {"name": "Module PV1", "sa": "Module PV1", "via_device": "inverter_123", "ids": ["input_pv1_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) + assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv1_123", "val_tpl": "{{ (value_json['pv1']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV1", "sa": "Module PV1", "via_device": "inverter_123", "ids": ["input_pv1_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) tests +=1 elif id == 'power_pv2_123': - assert comp == 'sensor' - assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv2_123", "val_tpl": "{{ (value_json['pv2']['Power'] | float)}}", "unit_of_meas": "W", "ic": "mdi:gauge", "dev": {"name": "Module PV2", "sa": "Module PV2", "via_device": "inverter_123", "ids": ["input_pv2_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) - tests +=1 + assert False # if we haven't received and parsed a control data msg, we don't know the number of inputs. In this case we only register the first one!! + elif id == 'signal_123': assert comp == 'sensor' - assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller", "sa": "Controller", "ids": ["controller_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) + assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller", "sa": "Controller", "via_device": "proxy", "ids": ["controller_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) tests +=1 - assert tests==5 + assert tests==4 def test_build_ha_conf2(ContrDataSeq, InvDataSeq): i = Infos() @@ -89,7 +88,7 @@ def test_build_ha_conf2(ContrDataSeq, InvDataSeq): pass tests = 0 - for d_json, comp, id in i.ha_confs(prfx="tsun/garagendach/", snr='123'): + for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", inv_snr='123', inv_node_id="garagendach/",proxy_node_id = 'proxy/', proxy_unique_id = '456'): if id == 'out_power_123': assert comp == 'sensor' @@ -103,16 +102,16 @@ def test_build_ha_conf2(ContrDataSeq, InvDataSeq): elif id == 'power_pv1_123': assert comp == 'sensor' - assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv1_123", "val_tpl": "{{ (value_json['pv1']['Power'] | float)}}", "unit_of_meas": "W", "ic": "mdi:gauge", "dev": {"name": "Module PV1", "sa": "Module PV1", "via_device": "inverter_123", "ids": ["input_pv1_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) + assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv1_123", "val_tpl": "{{ (value_json['pv1']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV1", "sa": "Module PV1", "via_device": "inverter_123", "ids": ["input_pv1_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) tests +=1 elif id == 'power_pv2_123': assert comp == 'sensor' - assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv2_123", "val_tpl": "{{ (value_json['pv2']['Power'] | float)}}", "unit_of_meas": "W", "ic": "mdi:gauge", "dev": {"name": "Module PV2", "sa": "Module PV2", "via_device": "inverter_123", "ids": ["input_pv2_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) + assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv2_123", "val_tpl": "{{ (value_json['pv2']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV2", "sa": "Module PV2", "via_device": "inverter_123", "ids": ["input_pv2_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) tests +=1 elif id == 'signal_123': assert comp == 'sensor' - assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller", "sa": "Controller", "mdl": "RSW-1-10001", "mf": "Raymon", "sw": "RSW_400_V1.00.06", "ids": ["controller_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) + assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller", "sa": "Controller", "via_device": "proxy", "mdl": "RSW-1-10001", "mf": "Raymon", "sw": "RSW_400_V1.00.06", "ids": ["controller_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) tests +=1 assert tests==5 diff --git a/system_tests/test_tcp_socket.py b/system_tests/test_tcp_socket.py new file mode 100644 index 0000000..a50bb98 --- /dev/null +++ b/system_tests/test_tcp_socket.py @@ -0,0 +1,199 @@ +# test_with_pytest.py and scapy +# +import pytest, socket, time +#from scapy.all import * +#from scapy.layers.inet import IP, TCP, TCP_client + +def get_sn() -> bytes: + return b'R170000000000001' + +def get_inv_no() -> bytes: + return b'T170000000000001' + +def get_invalid_sn(): + return b'R170000000000002' + + +@pytest.fixture +def MsgContactInfo(): # Contact Info message + return b'\x00\x00\x00\x2c\x10'+get_sn()+b'\x91\x00\x08solarhub\x0fsolarhub\x40123456' + +@pytest.fixture +def MsgContactResp(): # Contact Response message + return b'\x00\x00\x00\x14\x10'+get_sn()+b'\x99\x00\x01' + +@pytest.fixture +def MsgContactInfo2(): # Contact Info message + return b'\x00\x00\x00\x2c\x10'+get_invalid_sn()+b'\x91\x00\x08solarhub\x0fsolarhub\x40123456' + +@pytest.fixture +def MsgContactResp2(): # Contact Response message + return b'\x00\x00\x00\x14\x10'+get_invalid_sn()+b'\x99\x00\x01' + +@pytest.fixture +def MsgTimeStampReq(): # Get Time Request message + return b'\x00\x00\x00\x13\x10'+get_sn()+b'\x91\x22' + +@pytest.fixture +def MsgTimeStampResp(): # Get Time Resonse message + return b'\x00\x00\x00\x1b\x10'+get_sn()+b'\x99\x22\x00\x00\x01\x89\xc6\x63\x4d\x80' + +@pytest.fixture +def MsgContollerInd(): # Data indication from the controller + msg = b'\x00\x00\x01\x2f\x10'+ get_sn() + b'\x91\x71\x0e\x10\x00\x00\x10'+get_sn() + msg += b'\x01\x00\x00\x01\x89\xc6\x63\x55\x50' + msg += b'\x00\x00\x00\x15\x00\x09\x2b\xa8\x54\x10\x52\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x30\x36\x00\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f' + msg += b'\x6e\x00\x09\x2f\x90\x54\x0b\x52\x53\x57\x2d\x31\x2d\x31\x30\x30\x30\x31\x00\x09\x5a\x88\x54\x0f\x74\x2e\x72\x61\x79\x6d\x6f\x6e\x69\x6f\x74\x2e\x63\x6f\x6d\x00\x09\x5a\xec\x54' + msg += b'\x1c\x6c\x6f\x67\x67\x65\x72\x2e\x74\x61\x6c\x65\x6e\x74\x2d\x6d\x6f\x6e\x69\x74\x6f\x72\x69\x6e\x67\x2e\x63\x6f\x6d\x00\x0d\x00\x20\x49\x00\x00\x00\x01\x00\x0c\x35\x00\x49\x00' + msg += b'\x00\x00\x64\x00\x0c\x96\xa8\x49\x00\x00\x00\x1d\x00\x0c\x7f\x38\x49\x00\x00\x00\x01\x00\x0c\xfc\x38\x49\x00\x00\x00\x01\x00\x0c\xf8\x50\x49\x00\x00\x01\x2c\x00\x0c\x63\xe0\x49' + msg += b'\x00\x00\x00\x00\x00\x0c\x67\xc8\x49\x00\x00\x00\x00\x00\x0c\x50\x58\x49\x00\x00\x00\x01\x00\x09\x5e\x70\x49\x00\x00\x13\x8d\x00\x09\x5e\xd4\x49\x00\x00\x13\x8d\x00\x09\x5b\x50' + msg += b'\x49\x00\x00\x00\x02\x00\x0d\x04\x08\x49\x00\x00\x00\x00\x00\x07\xa1\x84\x49\x00\x00\x00\x01\x00\x0c\x50\x59\x49\x00\x00\x00\x4c\x00\x0d\x1f\x60\x49\x00\x00\x00\x00' + return msg + +@pytest.fixture +def MsgInvData(): # Data indication from the controller + msg = b'\x00\x00\x00\x8b\x10'+ get_sn() + b'\x91\x04\x01\x90\x00\x01\x10'+get_inv_no() + msg += b'\x01\x00\x00\x01\x89\xc6\x63\x61\x08' + msg += b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28' + msg += b'\x54\x10\x54\x31\x37\x45\x37\x33\x30\x37\x30\x32\x31\x44\x30\x30\x36\x41\x00\x00\x00\x32\x54\x0a\x54\x53\x4f\x4c\x2d\x4d\x53\x36\x30\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43' + return msg + +@pytest.fixture +def MsgInverterInd(): # Data indication from the inverter + msg = b'\x00\x00\x05\x02\x10'+ get_sn() + b'\x91\x04\x01\x90\x00\x01\x10'+get_inv_no() + msg += b'\x01\x00\x00\x01\x89\xc6\x63\x61\x08' + msg += b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00' + msg += b'\x00\x00\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00' + msg += b'\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53' + msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00\x00\x00\x01\xf5\x53\x00\x00\x00\x00' + msg += b'\x01\xf6\x53\x00\x00\x00\x00\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00\x00\x00' + msg += b'\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00' + msg += b'\x00\x00\x02\x02\x53\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00\x00\x00\x02\x5a' + msg += b'\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02' + msg += b'\x60\x53\x00\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02\x65\x53\x00\x00\x00\x00' + msg += b'\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00' + msg += b'\x00\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3\x53\x00\x00\x00\x00\x02\xc4\x53' + msg += b'\x00\x00\x00\x00\x02\xc5\x53\x00\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02\xca' + msg += b'\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53\x00\x00\x00\x00\x03\x20\x53\x00\x00\x00\x00\x03\x84\x53\x50\x11\x00\x00\x03\xe8\x46\x43\x61\x66\x66\x00' + msg += b'\x00\x04\x4c\x46\x3e\xeb\x85\x1f\x00\x00\x04\xb0\x46\x42\x48\x14\x7b\x00\x00\x05\x14\x53\x00\x17\x00\x00\x05\x78\x53\x00\x00\x00\x00\x05\xdc\x53\x02\x58\x00\x00\x06' + msg += b'\x40\x46\x42\xd3\x66\x66\x00\x00\x06\xa4\x46\x42\x06\x66\x66\x00\x00\x07\x08\x46\x3f\xf4\x7a\xe1\x00\x00\x07\x6c\x46\x42\x81\x00\x00\x00\x00\x07\xd0\x46\x42\x06\x00' + msg += b'\x00\x00\x00\x08\x34\x46\x3f\xae\x14\x7b\x00\x00\x08\x98\x46\x42\x36\xcc\xcd\x00\x00\x08\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x00\x00\x00\x00\x00\x00\x09\xc4' + msg += b'\x46\x00\x00\x00\x00\x00\x00\x0a\x28\x46\x00\x00\x00\x00\x00\x00\x0a\x8c\x46\x00\x00\x00\x00\x00\x00\x0a\xf0\x46\x00\x00\x00\x00\x00\x00\x0b\x54\x46\x3f\xd9\x99\x9a' + msg += b'\x00\x00\x0b\xb8\x46\x41\x8a\xe1\x48\x00\x00\x0c\x1c\x46\x3f\x8a\x3d\x71\x00\x00\x0c\x80\x46\x41\x1b\xd7\x0a\x00\x00\x0c\xe4\x46\x3f\x1e\xb8\x52\x00\x00\x0d\x48\x46' + msg += b'\x40\xf3\xd7\x0a\x00\x00\x0d\xac\x46\x00\x00\x00\x00\x00\x00\x0e\x10\x46\x00\x00\x00\x00\x00\x00\x0e\x74\x46\x00\x00\x00\x00\x00\x00\x0e\xd8\x46\x00\x00\x00\x00\x00' + msg += b'\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0\x53\x00\x00\x00\x00\x10\x04\x53\x55\xaa\x00\x00\x10\x68\x53\x00\x00\x00\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00' + msg += b'\x00\x00\x11\x94\x53\x00\x00\x00\x00\x11\xf8\x53\xff\xff\x00\x00\x12\x5c\x53\xff\xff\x00\x00\x12\xc0\x53\x00\x02\x00\x00\x13\x24\x53\xff\xff\x00\x00\x13\x88\x53\xff' + msg += b'\xff\x00\x00\x13\xec\x53\xff\xff\x00\x00\x14\x50\x53\xff\xff\x00\x00\x14\xb4\x53\xff\xff\x00\x00\x15\x18\x53\xff\xff\x00\x00\x15\x7c\x53\x00\x00\x00\x00\x27\x10\x53' + msg += b'\x00\x02\x00\x00\x27\x74\x53\x00\x3c\x00\x00\x27\xd8\x53\x00\x68\x00\x00\x28\x3c\x53\x05\x00\x00\x00\x28\xa0\x46\x43\x79\x00\x00\x00\x00\x29\x04\x46\x43\x48\x00\x00' + msg += b'\x00\x00\x29\x68\x46\x42\x48\x33\x33\x00\x00\x29\xcc\x46\x42\x3e\x3d\x71\x00\x00\x2a\x30\x53\x00\x01\x00\x00\x2a\x94\x46\x43\x37\x00\x00\x00\x00\x2a\xf8\x46\x42\xce' + msg += b'\x00\x00\x00\x00\x2b\x5c\x53\x00\x96\x00\x00\x2b\xc0\x53\x00\x10\x00\x00\x2c\x24\x46\x43\x90\x00\x00\x00\x00\x2c\x88\x46\x43\x95\x00\x00\x00\x00\x2c\xec\x53\x00\x06' + msg += b'\x00\x00\x2d\x50\x53\x00\x06\x00\x00\x2d\xb4\x46\x43\x7d\x00\x00\x00\x00\x2e\x18\x46\x42\x3d\xeb\x85\x00\x00\x2e\x7c\x46\x42\x3d\xeb\x85\x00\x00\x2e\xe0\x53\x00\x03' + msg += b'\x00\x00\x2f\x44\x53\x00\x03\x00\x00\x2f\xa8\x46\x42\x4d\xeb\x85\x00\x00\x30\x0c\x46\x42\x4d\xeb\x85\x00\x00\x30\x70\x53\x00\x03\x00\x00\x30\xd4\x53\x00\x03\x00\x00' + msg += b'\x31\x38\x46\x42\x08\x00\x00\x00\x00\x31\x9c\x53\x00\x05\x00\x00\x32\x00\x53\x04\x00\x00\x00\x32\x64\x53\x00\x01\x00\x00\x32\xc8\x53\x13\x9c\x00\x00\x33\x2c\x53\x0f' + msg += b'\xa0\x00\x00\x33\x90\x53\x00\x4f\x00\x00\x33\xf4\x53\x00\x66\x00\x00\x34\x58\x53\x03\xe8\x00\x00\x34\xbc\x53\x04\x00\x00\x00\x35\x20\x53\x00\x00\x00\x00\x35\x84\x53' + msg += b'\x00\x00\x00\x00\x35\xe8\x53\x00\x00\x00\x00\x36\x4c\x53\x00\x00\x00\x01\x38\x80\x53\x00\x02\x00\x01\x38\x81\x53\x00\x01\x00\x01\x38\x82\x53\x00\x01\x00\x01\x38\x83' + msg += b'\x53\x00\x00' + + return msg + + +@pytest.fixture(scope="session") +def ClientConnection(): + #host = '172.16.30.7' + host = 'logger.talent-monitoring.com' + #host = '127.0.0.1' + port = 5005 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((host, port)) + s.settimeout(1) + yield s + s.close() + +def tempClientConnection(): + #host = '172.16.30.7' + host = 'logger.talent-monitoring.com' + #host = '127.0.0.1' + port = 5005 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((host, port)) + s.settimeout(1) + yield s + s.close() + +def test_open_close(): + try: + for s in tempClientConnection(): + pass + except: + assert False + assert True + +def test_send_contact_info1(ClientConnection, MsgContactInfo, MsgContactResp): + s = ClientConnection + try: + s.sendall(MsgContactInfo) + data = s.recv(1024) + except TimeoutError: + pass + assert data == MsgContactResp + +def test_send_contact_info2(ClientConnection, MsgContactInfo2, MsgContactInfo, MsgContactResp): + s = ClientConnection + try: + s.sendall(MsgContactInfo2) + data = s.recv(1024) + except TimeoutError: + assert True + else: + assert False + + try: + s.sendall(MsgContactInfo) + data = s.recv(1024) + except TimeoutError: + pass + assert data == MsgContactResp + + + +def test_send_contact_resp(ClientConnection, MsgContactResp): + s = ClientConnection + try: + s.sendall(MsgContactResp) + data = s.recv(1024) + except TimeoutError: + assert True + else: + assert data =='' + +def test_send_ctrl_data(ClientConnection, MsgTimeStampReq, MsgTimeStampResp, MsgContollerInd): + s = ClientConnection + try: + s.sendall(MsgTimeStampReq) + data = s.recv(1024) + except TimeoutError: + pass + # time.sleep(2.5) + # assert data == MsgTimeStampResp + try: + s.sendall(MsgContollerInd) + data = s.recv(1024) + except TimeoutError: + pass + +def test_send_inv_data(ClientConnection, MsgTimeStampReq, MsgTimeStampResp, MsgInvData, MsgInverterInd): + s = ClientConnection + try: + s.sendall(MsgTimeStampReq) + data = s.recv(1024) + except TimeoutError: + pass + # time.sleep(32.5) + # assert data == MsgTimeStampResp + try: + s.sendall(MsgInvData) + data = s.recv(1024) + s.sendall(MsgInverterInd) + data = s.recv(1024) + except TimeoutError: + pass