From a9dc7e6847193d530b20300586c92c281c2e0032 Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Tue, 3 Sep 2024 18:58:24 +0200 Subject: [PATCH] Dev 0.11 (#181) * Sonar qube 6 (#174) * test class ModbusConn * Sonar qube 3 (#178) * add more unit tests * GEN3: don't crash on overwritten msg in the receive buffer * improve test coverage und reduce test delays * reduce cognitive complexity --- CHANGELOG.md | 1 + app/src/async_stream.py | 1 + app/src/gen3/connection_g3.py | 4 +- app/src/gen3/talent.py | 7 + app/src/gen3plus/connection_g3p.py | 4 +- app/src/infos.py | 198 ++++++++++++++++------------- app/tests/test_modbus_tcp.py | 58 +++++---- app/tests/test_mqtt.py | 2 +- app/tests/test_talent.py | 181 ++++++++++++++++++++++++++ 9 files changed, 334 insertions(+), 122 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e41d65b..3db2d75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- GEN3: don't crash on overwritten msg in the receive buffer - Reading the version string from the image updates it even if the image is re-pulled without re-deployment ## [0.10.1] - 2024-08-10 diff --git a/app/src/async_stream.py b/app/src/async_stream.py index 5892ba2..ae7e584 100644 --- a/app/src/async_stream.py +++ b/app/src/async_stream.py @@ -147,6 +147,7 @@ class AsyncStream(): logger.error( f"Exception for {self.addr}:\n" f"{traceback.format_exc()}") + await asyncio.sleep(0) # be cooperative to other task async def async_write(self, headline: str = 'Transmit to ') -> None: """Async write handler to transmit the send_buffer""" diff --git a/app/src/gen3/connection_g3.py b/app/src/gen3/connection_g3.py index 66683fa..5aea231 100644 --- a/app/src/gen3/connection_g3.py +++ b/app/src/gen3/connection_g3.py @@ -25,10 +25,10 @@ class ConnectionG3(AsyncStream, Talent): # logger.info(f'AsyncStream refs: {gc.get_referrers(self)}') async def async_create_remote(self) -> None: - pass # virtual interface + pass # virtual interface # pragma: no cover async def async_publ_mqtt(self) -> None: - pass # virtual interface + pass # virtual interface # pragma: no cover def healthy(self) -> bool: logger.debug('ConnectionG3 healthy()') diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index 611c9fc..015efd9 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -294,6 +294,13 @@ class Talent(Message): result = struct.unpack_from('!lB', buf, 0) msg_len = result[0] # len of complete message id_len = result[1] # len of variable id string + if id_len > 17: + logger.warning(f'len of ID string must == 16 but is {id_len}') + self.inc_counter('Invalid_Msg_Format') + + # erase broken recv buffer + self._recv_buffer = bytearray() + return hdr_len = 5+id_len+2 diff --git a/app/src/gen3plus/connection_g3p.py b/app/src/gen3plus/connection_g3p.py index dc4eca1..89dfc1a 100644 --- a/app/src/gen3plus/connection_g3p.py +++ b/app/src/gen3plus/connection_g3p.py @@ -31,10 +31,10 @@ class ConnectionG3P(AsyncStream, SolarmanV5): # logger.info(f'AsyncStream refs: {gc.get_referrers(self)}') async def async_create_remote(self) -> None: - pass # virtual interface + pass # virtual interface # pragma: no cover async def async_publ_mqtt(self) -> None: - pass # virtual interface + pass # virtual interface # pragma: no cover def healthy(self) -> bool: logger.debug('ConnectionG3P healthy()') diff --git a/app/src/infos.py b/app/src/infos.py index f5f529a..2192b5b 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -427,100 +427,120 @@ class Infos: return None elif singleton: return None - prfx = ha_prfx + node_id # 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 = {} - if 'name' in ha: - attr['name'] = ha['name'] - else: - attr['name'] = row['name'][-1] - 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'] - elif 'fmt' in ha: - 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") - # 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'] # icon for the entity - if 'nat_prc' in ha: # pragma: no cover - 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['en'] = ha['en'] - if 'dev' in ha: - device = self.info_devs[ha['dev']] - if 'dep' in device and self.ignore_this_device(device['dep']): # noqa: E501 - return None - 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'])): - dev['name'] = 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 - 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}" - else: - self.inc_counter('Internal_Error') - 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', 'manufacturer', '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}"] - attr['dev'] = dev - origin = {} - origin['name'] = self.app_name - origin['sw'] = self.version - 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") - return json.dumps(attr), component, node_id, attr['uniq_id'] + return self.__ha_conf(row, key, ha_prfx, node_id, snr, sug_area) return None + def __ha_conf(self, row, key, ha_prfx, node_id, snr, + sug_area: str) -> tuple[str, str, str, str] | None: + ha = row['ha'] + if 'comp' in ha: + component = ha['comp'] + else: + component = 'sensor' + attr = self.__build_attr(row, key, ha_prfx, node_id, snr) + if 'dev' in ha: + device = self.info_devs[ha['dev']] + if 'dep' in device and self.ignore_this_device(device['dep']): # noqa: E501 + return None + attr['dev'] = self.__build_dev(device, key, ha, snr, + sug_area) + attr['o'] = self.__build_origin() + + else: + self.inc_counter('Internal_Error') + logging.error(f"Infos.info_defs: the row for {key} " + "missing 'dev' value for ha register") + return json.dumps(attr), component, node_id, attr['uniq_id'] + + def __build_attr(self, row, key, ha_prfx, node_id, snr): + attr = {} + ha = row['ha'] + if 'name' in ha: + attr['name'] = ha['name'] + else: + attr['name'] = row['name'][-1] + prfx = ha_prfx + node_id + 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'] + elif 'fmt' in ha: + 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") + # 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'] # icon for the entity + if 'nat_prc' in ha: # pragma: no cover + 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['en'] = ha['en'] + return attr + + def __build_dev(self, device, key, ha, snr, sug_area): + dev = {} + singleton = '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): + dev['name'] = device['name'] + dev['sa'] = device['name'] + else: + dev['name'] = device['name']+' - '+sug_area + dev['sa'] = device['name']+' - '+sug_area + self.__add_via_dev(dev, device, key, snr) + for key in ('mdl', 'mf', 'sw', 'hw'): # add optional + # values fpr 'modell', 'manufacturer', '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: + dev['ids'] = [f"{ha['dev']}"] + else: + dev['ids'] = [f"{ha['dev']}_{snr}"] + return dev + + def __add_via_dev(self, dev, device, key, snr): + 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] + if 'singleton' in via_dev and via_dev['singleton']: + dev['via_device'] = via + else: + dev['via_device'] = f"{via}_{snr}" + else: + self.inc_counter('Internal_Error') + logging.error(f"Infos.info_defs: the row for " + f"{key} has an invalid via value: " + f"{via}") + + def __build_origin(self): + origin = {} + origin['name'] = self.app_name + origin['sw'] = self.version + return origin + def ha_remove(self, key, node_id, snr) -> tuple[str, str, str, str] | None: '''Method to build json unregister struct for home-assistant to remove topics per auto configuration. Only for inverer topics. diff --git a/app/tests/test_modbus_tcp.py b/app/tests/test_modbus_tcp.py index c2ed8a2..f9ec1ef 100644 --- a/app/tests/test_modbus_tcp.py +++ b/app/tests/test_modbus_tcp.py @@ -4,6 +4,7 @@ import asyncio from mock import patch from enum import Enum +from enum import Enum from app.src.singleton import Singleton from app.src.config import Config from app.src.infos import Infos @@ -11,6 +12,10 @@ from app.src.mqtt import Mqtt from app.src.messages import Message, State from app.src.inverter import Inverter from app.src.modbus_tcp import ModbusConn, ModbusTcp +from app.src.mqtt import Mqtt +from app.src.messages import Message, State +from app.src.inverter import Inverter +from app.src.modbus_tcp import ModbusConn, ModbusTcp pytest_plugins = ('pytest_asyncio',) @@ -77,6 +82,7 @@ class TestType(Enum): test = TestType.RD_TEST_0_BYTES + class FakeReader(): def __init__(self): self.on_recv = asyncio.Event() @@ -110,7 +116,7 @@ class FakeWriter(): @pytest.fixture def patch_open(): async def new_conn(conn): - await asyncio.sleep(0.01) + await asyncio.sleep(0) return FakeReader(), FakeWriter() def new_open(host: str, port: int): @@ -127,6 +133,11 @@ def patch_no_mqtt(): with patch.object(Mqtt, 'publish') as conn: yield conn +@pytest.fixture +def patch_no_mqtt(): + with patch.object(Mqtt, 'publish') as conn: + yield conn + @pytest.mark.asyncio async def test_modbus_conn(patch_open): @@ -161,12 +172,12 @@ async def test_modbus_cnf1(config_conn, patch_open): assert Infos.stat['proxy']['Inverter_Cnt'] == 0 loop = asyncio.get_event_loop() ModbusTcp(loop) - await asyncio.sleep(0.1) + await asyncio.sleep(0.01) for m in Message: if (m.node_id == 'inv_2'): assert False - await asyncio.sleep(0.1) + await asyncio.sleep(0.01) assert Infos.stat['proxy']['Inverter_Cnt'] == 0 @pytest.mark.asyncio @@ -181,7 +192,7 @@ async def test_modbus_cnf2(config_conn, patch_no_mqtt, patch_open): assert Infos.stat['proxy']['Inverter_Cnt'] == 0 ModbusTcp(asyncio.get_event_loop()) - await asyncio.sleep(0.1) + await asyncio.sleep(0.01) test = 0 for m in Message: if (m.node_id == 'inv_2'): @@ -192,14 +203,13 @@ async def test_modbus_cnf2(config_conn, patch_no_mqtt, patch_open): del m assert 1 == test - await asyncio.sleep(0.1) + await asyncio.sleep(0.01) assert Infos.stat['proxy']['Inverter_Cnt'] == 0 # check that the connection is released for m in Message: if (m.node_id == 'inv_2'): assert False - @pytest.mark.asyncio async def test_modbus_cnf3(config_conn, patch_no_mqtt, patch_open): _ = config_conn @@ -211,32 +221,24 @@ async def test_modbus_cnf3(config_conn, patch_no_mqtt, patch_open): test = TestType.RD_TEST_0_BYTES assert Infos.stat['proxy']['Inverter_Cnt'] == 0 - ModbusTcp(asyncio.get_event_loop(), tim_restart= 0.1) - await asyncio.sleep(0.1) + ModbusTcp(asyncio.get_event_loop(), tim_restart= 0) + await asyncio.sleep(0.01) test = 0 for m in Message: if (m.node_id == 'inv_2'): assert Infos.stat['proxy']['Inverter_Cnt'] == 1 - m.shutdown_started = False - m.reader.on_recv.set() test += 1 - await asyncio.sleep(0.1) - assert m.state == State.closed - - assert 1 == test - await asyncio.sleep(0.1) - assert Infos.stat['proxy']['Inverter_Cnt'] == 1 - # check that the connection is released - for m in Message: - if (m.node_id == 'inv_2'): - test += 1 - m.shutdown_started = True - m.reader.on_recv.set() - del m + if test == 1: + m.shutdown_started = False + m.reader.on_recv.set() + await asyncio.sleep(0.1) + assert m.state == State.closed + await asyncio.sleep(0.1) + else: + m.shutdown_started = True + m.reader.on_recv.set() + del m - assert 3 == test - await asyncio.sleep(0.1) + assert 2 == test + await asyncio.sleep(0.01) assert Infos.stat['proxy']['Inverter_Cnt'] == 0 - for m in Message: - if (m.node_id == 'inv_2'): - assert False diff --git a/app/tests/test_mqtt.py b/app/tests/test_mqtt.py index 1d7c5dd..3072627 100644 --- a/app/tests/test_mqtt.py +++ b/app/tests/test_mqtt.py @@ -97,7 +97,7 @@ async def test_mqtt_no_config(config_no_conn): try: m = Mqtt(cb) assert m.task - await asyncio.sleep(1) + await asyncio.sleep(0) assert not on_connect.is_set() try: await m.publish('homeassistant/status', 'online') diff --git a/app/tests/test_talent.py b/app/tests/test_talent.py index 13f462e..16bb8d8 100644 --- a/app/tests/test_talent.py +++ b/app/tests/test_talent.py @@ -526,6 +526,166 @@ def broken_recv_buf(): # There are two message in the buffer, but the second has msg += b'\x08\x00\x00\x00\x00\x31' return msg +@pytest.fixture +def multiple_recv_buf(): # There are three message in the buffer, but the second has overwritten the first partly + msg = b'\x00\x00\x05\x02\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001' + msg += b'\x01\x00\x00\x01\x89\xc6\x63\x61\x08' + msg += b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01' + msg += b'\x00\x00\x00\xc8\x53\x00\x00\x00\x00\x01\x2c\x53\x00\x02\x00\x00' # | ....S.....,S.... + msg += b'\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00\x00\x00' # | ..I........S.... + msg += b'\x13\x10\x52\x31\x37\x45\x37\x33\x30\x37\x30\x32\x31\x44\x30\x30' # | ..R17E7307021D00 + msg += b'\x36\x41\x91\x22\x00\x00\x03\xbf\x10\x52\x31\x37\x45\x37\x33\x30' # | 6A.".....R17E730 + msg += b'\x37\x30\x32\x31\x44\x30\x30\x36\x41\x91\x71\x0e\x10\x00\x00\x10' # | 7021D006A.q..... + msg += b'\x52\x31\x37\x45\x37\x33\x30\x37\x30\x32\x31\x44\x30\x30\x36\x41' # | R17E7307021D006A + msg += b'\x01\x00\x00\x01\x91\xa3\xfe\xaf\x98\x00\x00\x00\x35\x00\x09\x2b' # | ............5..+ + msg += b'\xa8\x54\x10\x52\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30' # | .T.RSW_400_V1.00 + msg += b'\x2e\x31\x37\x00\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f\x6e\x00' # | .17..'.T.Raymon. + msg += b'\x09\x2f\x90\x54\x0b\x52\x53\x57\x2d\x31\x2d\x31\x30\x30\x30\x31' # | ./.T.RSW-1-10001 + msg += b'\x00\x09\x5a\x88\x54\x0f\x74\x2e\x72\x61\x79\x6d\x6f\x6e\x69\x6f' # | ..Z.T.t.raymonio + msg += b'\x74\x2e\x63\x6f\x6d\x00\x09\x5a\xec\x54\x1c\x6c\x6f\x67\x67\x65' # | t.com..Z.T.logge + msg += b'\x72\x2e\x74\x61\x6c\x65\x6e\x74\x2d\x6d\x6f\x6e\x69\x74\x6f\x72' # | r.talent-monitor + msg += b'\x69\x6e\x67\x2e\x63\x6f\x6d\x00\x0d\x2f\x00\x54\x10\xff\xff\xff' # | ing.com../.T.... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x32' # | ...............2 + msg += b'\xe8\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .T.............. + msg += b'\xff\xff\xff\x00\x0d\x36\xd0\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .....6.T........ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x3a\xb8\x54\x10\xff' # | ...........:.T.. + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................ + msg += b'\x0d\x3e\xa0\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .>.T............ + msg += b'\xff\xff\xff\xff\xff\x00\x0d\x42\x88\x54\x10\xff\xff\xff\xff\xff' # | .......B.T...... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x46\x70\x54' # | .............FpT + msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\x00\x0d\x4a\x58\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...JXT.......... + msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x4e\x40\x54\x10\xff\xff\xff' # | .........N@T.... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x52' # | ...............R + msg += b'\x28\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | (T.............. + msg += b'\xff\xff\xff\x00\x0d\x56\x10\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .....V.T........ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x59\xf8\x54\x10\xff' # | ...........Y.T.. + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................ + msg += b'\x0d\x5d\xe0\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .].T............ + msg += b'\xff\xff\xff\xff\xff\x00\x0d\x61\xc8\x54\x10\xff\xff\xff\xff\xff' # | .......a.T...... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x65\xb0\x54' # | .............e.T + msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\x00\x0d\x69\x98\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ...i.T.......... + msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x6d\x80\x54\x10\xff\xff\xff' # | .........m.T.... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x71' # | ...............q + msg += b'\x68\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | hT.............. + msg += b'\xff\xff\xff\x00\x0d\x75\x50\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .....uPT........ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x79\x38\x54\x10\xff' # | ...........y8T.. + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................ + msg += b'\x0d\x7d\x20\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .} T............ + msg += b'\xff\xff\xff\xff\xff\x00\x0d\x81\x08\x54\x10\xff\xff\xff\xff\xff' # | .........T...... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x84\xf0\x54' # | ...............T + msg += b'\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | ................ + msg += b'\xff\x00\x0d\x88\xd8\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .....T.......... + msg += b'\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x8c\xc0\x54\x10\xff\xff\xff' # | ...........T.... + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x90' # | ................ + msg += b'\xa8\x54\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # | .T.............. + msg += b'\xff\xff\xff\x00\x0d\x94\x90\x54\x10\xff\xff\xff\xff\xff\xff\xff' # | .......T........ + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0d\x98\x78\x54\x10\xff' # | ............xT.. + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00' # | ................ + msg += b'\x0d\x00\x20\x49\x00\x00\x00\x01\x00\x0c\x35\x00\x49\x00\x00\x00' # | .. I......5.I... + msg += b'\x28\x00\x0c\x96\xa8\x49\x00\x00\x01\x69\x00\x0c\x7f\x38\x49\x00' # | (....I...i...8I. + msg += b'\x00\x00\x01\x00\x0c\xfc\x38\x49\x00\x00\x00\x01\x00\x0c\xf8\x50' # | ......8I.......P + msg += b'\x49\x00\x00\x01\x2c\x00\x0c\x63\xe0\x49\x00\x00\x00\x00\x00\x0c' # | I...,..c.I...... + msg += b'\x67\xc8\x49\x00\x00\x00\x00\x00\x0c\x50\x58\x49\x00\x00\x00\x01' # | g.I......PXI.... + msg += b'\x00\x09\x5e\x70\x49\x00\x00\x13\x8d\x00\x09\x5e\xd4\x49\x00\x00' # | ..^pI......^.I.. + msg += b'\x13\x8d\x00\x09\x5b\x50\x49\x00\x00\x00\x02\x00\x0d\x04\x08\x49' # | ....[PI........I + msg += b'\x00\x00\x00\x00\x00\x07\xa1\x84\x49\x00\x00\x00\x01\x00\x0c\x50' # | ........I......P + msg += b'\x59\x49\x00\x00\x00\x3e\x00\x0d\x1f\x60\x49\x00\x00\x00\x00\x00' # | YI...>...`I..... + msg += b'\x0d\x23\x48\x49\xff\xff\xff\xff\x00\x0d\x27\x30\x49\xff\xff\xff' # | .#HI......'0I... + msg += b'\xff\x00\x0d\x2b\x18\x4c\x00\x00\x00\x00\xff\xff\xff\xff\x00\x0c' # | ...+.L.......... + msg += b'\xa2\x60\x49\x00\x00\x00\x00\x00\x00\x05\x02\x10\x52\x31\x37\x45' # | .`I.........R17E + msg += b'\x37\x33\x30\x37\x30\x32\x31\x44\x30\x30\x36\x41\x91\x04\x01\x90' # | 7307021D006A.... + msg += b'\x00\x01\x10\x54\x31\x37\x45\x37\x33\x30\x37\x30\x32\x31\x44\x30' # | ...T17E7307021D0 + msg += b'\x30\x36\x41\x01\x00\x00\x01\x91\xa3\xfe\xb3\x80\x00\x00\x00\xa3' # | 06A............. + msg += b'\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x00\x00\x00' # | ...dS......S.... + msg += b'\x01\x2c\x53\x00\x02\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00' # | .,S......I...... + msg += b'\x01\x91\x53\x00\x00\x00\x00\x01\x92\x53\x00\x00\x00\x00\x01\x93' # | ..S......S...... + msg += b'\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00' # | S......S......S. + msg += b'\x00\x00\x00\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00\x00\x00' # | .....S......S... + msg += b'\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01' # | ...S......S..... + msg += b'\x9a\x53\x00\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x01\x9d\x53\x00\x00\x00\x00\x01\x9e\x53\x00\x00' # | ......S......S.. + msg += b'\x00\x00\x01\x9f\x53\x00\x00\x00\x00\x01\xa0\x53\x00\x00\x00\x00' # | ....S......S.... + msg += b'\x01\xf4\x49\x00\x00\x00\x00\x00\x00\x01\xf5\x53\x00\x00\x00\x00' # | ..I........S.... + msg += b'\x01\xf6\x53\x00\x00\x00\x00\x01\xf7\x53\x00\x00\x00\x00\x01\xf8' # | ..S......S...... + msg += b'\x53\x00\x00\x00\x00\x01\xf9\x53\x00\x00\x00\x00\x01\xfa\x53\x00' # | S......S......S. + msg += b'\x00\x00\x00\x01\xfb\x53\x00\x00\x00\x00\x01\xfc\x53\x00\x00\x00' # | .....S......S... + msg += b'\x00\x01\xfd\x53\x00\x00\x00\x00\x01\xfe\x53\x00\x00\x00\x00\x01' # | ...S......S..... + msg += b'\xff\x53\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x01\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x02\x02\x53\x00\x00\x00\x00\x02\x03\x53\x00\x00' # | ......S......S.. + msg += b'\x00\x00\x02\x04\x53\x00\x00\x00\x00\x02\x58\x49\x00\x00\x00\x00' # | ....S.....XI.... + msg += b'\x00\x00\x02\x59\x53\x00\x00\x00\x00\x02\x5a\x53\x00\x00\x00\x00' # | ...YS.....ZS.... + msg += b'\x02\x5b\x53\x00\x00\x00\x00\x02\x5c\x53\x00\x00\x00\x00\x02\x5d' # | .[S.....\S.....] + msg += b'\x53\x00\x00\x00\x00\x02\x5e\x53\x00\x00\x00\x00\x02\x5f\x53\x00' # | S.....^S....._S. + msg += b'\x00\x00\x00\x02\x60\x53\x00\x00\x00\x00\x02\x61\x53\x00\x00\x00' # | ....`S.....aS... + msg += b'\x00\x02\x62\x53\x00\x00\x00\x00\x02\x63\x53\x00\x00\x00\x00\x02' # | ..bS.....cS..... + msg += b'\x64\x53\x00\x00\x00\x00\x02\x65\x53\x00\x00\x00\x00\x02\x66\x53' # | dS.....eS.....fS + msg += b'\x00\x00\x00\x00\x02\x67\x53\x00\x00\x00\x00\x02\x68\x53\x00\x00' # | .....gS.....hS.. + msg += b'\x00\x00\x02\xbc\x49\x00\x00\x00\x00\x00\x00\x02\xbd\x53\x00\x00' # | ....I........S.. + msg += b'\x00\x00\x02\xbe\x53\x00\x00\x00\x00\x02\xbf\x53\x00\x00\x00\x00' # | ....S......S.... + msg += b'\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc2' # | ..S......S...... + msg += b'\x53\x00\x00\x00\x00\x02\xc3\x53\x00\x00\x00\x00\x02\xc4\x53\x00' # | S......S......S. + msg += b'\x00\x00\x00\x02\xc5\x53\x00\x00\x00\x00\x02\xc6\x53\x00\x00\x00' # | .....S......S... + msg += b'\x00\x02\xc7\x53\x00\x00\x00\x00\x02\xc8\x53\x00\x00\x00\x00\x02' # | ...S......S..... + msg += b'\xc9\x53\x00\x00\x00\x00\x02\xca\x53\x00\x00\x00\x00\x02\xcb\x53' # | .S......S......S + msg += b'\x00\x00\x00\x00\x02\xcc\x53\x00\x00\x00\x00\x03\x20\x53\x00\x00' # | ......S..... S.. + msg += b'\x00\x00\x03\x84\x53\x51\x09\x00\x00\x03\xe8\x46\x43\x65\x99\x9a' # | ....SQ.....FCe.. + msg += b'\x00\x00\x04\x4c\x46\x3e\xd7\x0a\x3d\x00\x00\x04\xb0\x46\x42\x48' # | ...LF>..=....FBH + msg += b'\x28\xf6\x00\x00\x05\x14\x53\x00\x1f\x00\x00\x05\x78\x53\x00\x00' # | (.....S.....xS.. + msg += b'\x00\x00\x05\xdc\x53\x02\x58\x00\x00\x06\x40\x46\x42\xc1\x33\x33' # | ....S.X...@FB.33 + msg += b'\x00\x00\x06\xa4\x46\x3f\x33\x33\x33\x00\x00\x07\x08\x46\x00\x00' # | ....F?333....F.. + msg += b'\x00\x00\x00\x00\x07\x6c\x46\x00\x00\x00\x00\x00\x00\x07\xd0\x46' # | .....lF........F + msg += b'\x42\x05\x99\x9a\x00\x00\x08\x34\x46\x40\x41\xeb\x85\x00\x00\x08' # | B......4F@A..... + msg += b'\x98\x46\x42\xcb\x66\x66\x00\x00\x08\xfc\x46\x00\x00\x00\x00\x00' # | .FB.ff....F..... + msg += b'\x00\x09\x60\x46\x00\x00\x00\x00\x00\x00\x09\xc4\x46\x00\x00\x00' # | ..`F........F... + msg += b'\x00\x00\x00\x0a\x28\x46\x00\x00\x00\x00\x00\x00\x0a\x8c\x46\x00' # | ....(F........F. + msg += b'\x00\x00\x00\x00\x00\x0a\xf0\x46\x00\x00\x00\x00\x00\x00\x0b\x54' # | .......F.......T + msg += b'\x46\x3f\x19\x99\x9a\x00\x00\x0b\xb8\x46\x43\xf3\x95\xc3\x00\x00' # | F?.......FC..... + msg += b'\x0c\x1c\x46\x00\x00\x00\x00\x00\x00\x0c\x80\x46\x43\x04\x4a\x3d' # | ..F........FC.J= + msg += b'\x00\x00\x0c\xe4\x46\x3f\x23\xd7\x0a\x00\x00\x0d\x48\x46\x43\xbf' # | ....F?#.....HFC. + msg += b'\x9e\xb8\x00\x00\x0d\xac\x46\x00\x00\x00\x00\x00\x00\x0e\x10\x46' # | ......F........F + msg += b'\x00\x00\x00\x00\x00\x00\x0e\x74\x46\x00\x00\x00\x00\x00\x00\x0e' # | .......tF....... + msg += b'\xd8\x46\x00\x00\x00\x00\x00\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f' # | .F.......=q..* + msg += b'\x30\x53\x00\x01\x00\x00\x2a\x94\x46\x43\x37\x00\x00\x00\x00\x2a' # | 0S....*.FC7....* + msg += b'\xf8\x46\x42\xce\x00\x00\x00\x00\x2b\x5c\x53\x00\x96\x00\x00\x2b' # | .FB.....+\S....+ + msg += b'\xc0\x53\x00\x10\x00\x00\x2c\x24\x46\x43\x90\x00\x00\x00\x00\x2c' # | .S....,$FC....., + msg += b'\x88\x46\x43\x95\x00\x00\x00\x00\x2c\xec\x53\x00\x06\x00\x00\x2d' # | .FC.....,.S....- + msg += b'\x50\x53\x00\x06\x00\x00\x2d\xb4\x46\x43\x7d\x00\x00\x00\x00\x2e' # | PS....-.FC}..... + msg += b'\x18\x46\x42\x3d\xeb\x85\x00\x00\x2e\x7c\x46\x42\x3d\xeb\x85\x00' # | .FB=.....|FB=... + msg += b'\x00\x2e\xe0\x53\x00\x03\x00\x00\x2f\x44\x53\x00\x03\x00\x00\x2f' # | ...S..../DS..../ + msg += b'\xa8\x46\x42\x4d\xeb\x85\x00\x00\x30\x0c\x46\x42\x4d\xeb\x85\x00' # | .FBM....0.FBM... + msg += b'\x00\x30\x70\x53\x00\x03\x00\x00\x30\xd4\x53\x00\x03\x00\x00\x31' # | .0pS....0.S....1 + msg += b'\x38\x46\x42\x08\x00\x00\x00\x00\x31\x9c\x53\x00\x05\x00\x00\x32' # | 8FB.....1.S....2 + msg += b'\x00\x53\x01\x61\x00\x00\x32\x64\x53\x00\x01\x00\x00\x32\xc8\x53' # | .S.a..2dS....2.S + msg += b'\x13\x9c\x00\x00\x33\x2c\x53\x0f\xa0\x00\x00\x33\x90\x53\x00\x4f' # | ....3,S....3.S.O + msg += b'\x00\x00\x33\xf4\x53\x00\x66\x00\x00\x34\x58\x53\x03\xe8\x00\x00' # | ..3.S.f..4XS.... + msg += b'\x34\xbc\x53\x04\x00\x00\x00\x35\x20\x53\x09\xc4\x00\x00\x35\x84' # | 4.S....5 S....5. + msg += b'\x53\x07\xc6\x00\x00\x35\xe8\x53\x13\x8c\x00\x00\x36\x4c\x53\x12' # | S....5.S....6LS. + msg += b'\x94\x00\x01\x38\x80\x53\x00\x02\x00\x01\x38\x81\x53\x00\x01\x00' # | ...8.S....8.S... + msg += b'\x01\x38\x82\x53\x00\x01\x00\x01\x38\x83\x53\x00\x00\x00\x00\x00' # | .8.S....8.S..... + msg += b'\x8b\x10\x52\x31\x37\x45\x37\x33\x30\x37\x30\x32\x31\x44\x30\x30' # | ..R17E7307021D00 + msg += b'\x36\x41\x91\x04\x01\x90\x00\x01\x10\x54\x31\x37\x45\x37\x33\x30' # | 6A.......T17E730 + msg += b'\x37\x30\x32\x31\x44\x30\x30\x36\x41\x01\x00\x00\x01\x91\xa3\xfe' # | 7021D006A....... + msg += b'\xb3\x80\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72' # | ..........T.Micr + msg += b'\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00' # | oinv....T.TSUN.. + msg += b'\x00\x1e\x54\x07\x56\x35\x2e\x31\x2e\x30\x39\x00\x00\x00\x28\x54' # | ..T.V5.1.09...(T + msg += b'\x10\x54\x31\x37\x45\x37\x33\x30\x37\x30\x32\x31\x44\x30\x30\x36' # | .T17E7307021D006 + msg += b'\x41\x00\x00\x00\x32\x54\x0a\x54\x53\x4f\x4c\x2d\x4d\x53\x36\x30' # | A...2T.TSOL-MS60 + msg += b'\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43' # | 0...