diff --git a/app/config/default_config.toml b/app/config/default_config.toml index 5d62a14..f308e24 100644 --- a/app/config/default_config.toml +++ b/app/config/default_config.toml @@ -36,5 +36,8 @@ inverters.allow_all = true # allow inverters, even if we have no inverter mapp #node_id = '' # Optional, MQTT replacement for inverters serial number #suggested_area = '' # Optional, suggested installation area for home-assistant - +[inverters."Y170000000000001"] +monitor_sn = 2070233889 +#node_id = '' # Optional, MQTT replacement for inverters serial number +#suggested_area = '' # Optional, suggested installation area for home-assistant diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index 149b142..1200da1 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -169,6 +169,7 @@ class SolarmanV5(Message): self.inc_counter('Invalid_Msg_Format') logger.debug(f'CRC {int(crc):#02x} {int(check):#08x}' f' Stop:{int(stop):#02x}') + self.inc_counter('Invalid_Msg_Format') return False return True diff --git a/app/src/infos.py b/app/src/infos.py index 2a5d818..d4be504 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -153,7 +153,7 @@ class Infos: Register.UNKNOWN_CTRL: {'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 Register.OTA_START_MSG: {'name': ['proxy', 'OTA_Start_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'ota_start_cmd_', 'fmt': '| int', 'name': 'OTA Start Cmd', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.SW_EXCEPTION: {'name': ['proxy', 'SW_Exception'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'sw_exception_', 'fmt': '| int', 'name': 'Internal SW Exception', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 - Register.INVALID_MSG_FMT: {'name': ['proxy', 'Invalid_Msg_Format'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_msg_fmt_', 'fmt': '| int', 'name': 'Invalid Msg Format', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.INVALID_MSG_FMT: {'name': ['proxy', 'Invalid_Msg_Format'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_msg_fmt_', 'fmt': '| int', 'name': 'Invalid Message Format', '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 diff --git a/app/tests/test_infos.py b/app/tests/test_infos.py index aedfa61..e3750ff 100644 --- a/app/tests/test_infos.py +++ b/app/tests/test_infos.py @@ -410,13 +410,13 @@ def test_statistic_counter(): assert val == None or val == 0 i.static_init() # initialize counter - assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0}}) + assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0}}) val = i.dev_value(Register.INVERTER_CNT) # valid and initiliazed addr assert val == 0 i.inc_counter('Inverter_Cnt') - assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 1, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0}}) + assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 1, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0}}) val = i.dev_value(Register.INVERTER_CNT) assert val == 1 diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py index af4931e..306f33d 100644 --- a/app/tests/test_solarman.py +++ b/app/tests/test_solarman.py @@ -1,5 +1,6 @@ import pytest, json from app.src.gen3plus.solarman_v5 import SolarmanV5 +from app.src.config import Config from app.src.infos import Infos # initialize the proxy statistics @@ -15,6 +16,8 @@ class MemoryStream(SolarmanV5): self.__chunk_idx = 0 self.msg_count = 0 self.addr = 'Test: SrvSide' + self.db.stat['proxy']['Invalid_Msg_Format'] = 0 + def append_msg(self, msg): self.__msg += msg @@ -48,7 +51,7 @@ class MemoryStream(SolarmanV5): def get_sn() -> bytes: - return b'\xc8\x1e\x4d\x7b' + return b'\x21\x43\x65\x7b' def get_inv_no() -> bytes: return b'T170000000000001' @@ -56,9 +59,16 @@ def get_inv_no() -> bytes: def get_invalid_sn(): return b'R170000000000002' +def correct_checksum(buf): + checksum = sum(buf[1:]) & 0xff + return checksum.to_bytes(length=1) + +def incorrect_checksum(buf): + checksum = (sum(buf[1:])+1) & 0xff + return checksum.to_bytes(length=1) @pytest.fixture -def TestMsg(): # Contact Info message +def DeviceIndMsg(): # 0x4110 msg = b'\xa5\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00' msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53' msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e' @@ -72,23 +82,215 @@ def TestMsg(): # Contact Info message msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' msg += b'\x00\x00\x00\x00\x00\x00\x00\x41\x6c\x6c\x69\x75\x73\x2d\x48\x6f' msg += b'\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3c' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += correct_checksum(msg) msg += b'\x15' return msg -def test_read_message(TestMsg): - m = MemoryStream(TestMsg, (0,)) +@pytest.fixture +def InvalidStartByte(): # 0x4110 + msg = b'\xa4\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00' + msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53' + msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e' + msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54\x31\x39\x32\x2e' + msg += b'\x31\x36\x38\x2e\x38\x30\x2e\x34\x39\x00\x00\x00\x0f\x00\x01\xb0' + msg += b'\x02\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x41\x6c\x6c\x69\x75\x73\x2d\x48\x6f' + msg += b'\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += correct_checksum(msg) + msg += b'\x15' + return msg + +@pytest.fixture +def InvalidStopByte(): # 0x4110 + msg = b'\xa5\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00' + msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53' + msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e' + msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54\x31\x39\x32\x2e' + msg += b'\x31\x36\x38\x2e\x38\x30\x2e\x34\x39\x00\x00\x00\x0f\x00\x01\xb0' + msg += b'\x02\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x41\x6c\x6c\x69\x75\x73\x2d\x48\x6f' + msg += b'\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += correct_checksum(msg) + msg += b'\x14' + return msg + +@pytest.fixture +def InvalidChecksum(): # 0x4110 + msg = b'\xa5\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00' + msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53' + msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e' + msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54\x31\x39\x32\x2e' + msg += b'\x31\x36\x38\x2e\x38\x30\x2e\x34\x39\x00\x00\x00\x0f\x00\x01\xb0' + msg += b'\x02\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x41\x6c\x6c\x69\x75\x73\x2d\x48\x6f' + msg += b'\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += incorrect_checksum(msg) + msg += b'\x15' + return msg + + +@pytest.fixture +def ConfigTsunAllowAll(): + Config.config = {'solarman':{'enabled': True}, 'inverters':{'allow_all':True}} + +@pytest.fixture +def ConfigNoTsunInv1(): + Config.config = {'solarman':{'enabled': False},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889,'node_id':'inv1','suggested_area':'roof'}}} + +@pytest.fixture +def ConfigTsunInv1(): + Config.config = {'solarman':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1','suggested_area':'roof'}}} + +def test_read_message(DeviceIndMsg): + m = MemoryStream(DeviceIndMsg, (0,)) m.read() # read complete msg, and dispatch msg assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert m.msg_count == 1 - # assert m.id_str == b"R170000000000001" - assert m.snr == 2068651720 - # assert m.unique_id == None - # assert int(m.ctrl)==145 - # assert m.msg_id==0 + assert m.header_len==11 + assert m.snr == 2070233889 + assert m.unique_id == None assert m.control == 0x4110 assert m.serial == 0x0100 assert m.data_len == 0xd4 assert m._forward_buffer==b'' + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 m.close() +def test_invalid_start_byte(InvalidStartByte): + m = MemoryStream(InvalidStartByte, (0,)) + m.read() # read complete msg, and dispatch msg + assert not m.header_valid # must be invalid, since start byte is wrong + assert m.msg_count == 0 + assert m.header_len==11 + assert m.snr == 2070233889 + assert m.unique_id == 0 + assert m.control == 0x4110 + assert m.serial == 0x0100 + assert m.data_len == 0xd4 + assert m._forward_buffer==b'' + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1 + + m.close() + +def test_invalid_stop_byte(InvalidStopByte): + m = MemoryStream(InvalidStopByte, (0,)) + m.read() # read complete msg, and dispatch msg + assert not m.header_valid # must be invalid, since start byte is wrong + assert m.msg_count == 1 # msg flush was called + assert m.header_len==11 + assert m.snr == 2070233889 + assert m.unique_id == 0 + assert m.control == 0x4110 + assert m.serial == 0x0100 + assert m.data_len == 0xd4 + assert m._forward_buffer==b'' + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1 + m.close() + +def test_invalid_checksum(InvalidChecksum): + m = MemoryStream(InvalidChecksum, (0,)) + m.read() # read complete msg, and dispatch msg + assert not m.header_valid # must be invalid, since start byte is wrong + assert m.msg_count == 1 # msg flush was called + assert m.header_len==11 + assert m.snr == 2070233889 + assert m.unique_id == 0 + assert m.control == 0x4110 + assert m.serial == 0x0100 + assert m.data_len == 0xd4 + assert m._forward_buffer==b'' + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1 + m.close() + +def test_read_message_twice(ConfigNoTsunInv1, DeviceIndMsg): + ConfigNoTsunInv1 + m = MemoryStream(DeviceIndMsg, (0,)) + m.append_msg(DeviceIndMsg) + m.read() # read complete msg, and dispatch msg + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 1 + assert m.header_len==11 + assert m.snr == 2070233889 + assert m.unique_id == '2070233889' + assert m.control == 0x4110 + assert m.serial == 0x0100 + assert m.data_len == 0xd4 + assert m._forward_buffer==b'' + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 + m.read() # read complete msg, and dispatch msg + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 2 + assert m.header_len==11 + assert m.snr == 2070233889 + assert m.unique_id == '2070233889' + assert m.control == 0x4110 + assert m.serial == 0x0100 + assert m.data_len == 0xd4 + assert m._forward_buffer==b'' + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 + m.close() + +def test_read_message_in_chunks(DeviceIndMsg): + m = MemoryStream(DeviceIndMsg, (4,11,0)) + m.read() # read 4 bytes, header incomplere + assert not m.header_valid # must be invalid, since header not complete + assert m.msg_count == 0 + m.read() # read missing bytes for complete header + assert m.header_valid # must be valid, since header is complete but not the msg + assert m.msg_count == 0 + assert m.header_len==11 + assert m.snr == 2070233889 + assert m.unique_id == 0 # should be None ? + assert m.control == 0x4110 + assert m.serial == 0x0100 + assert m.data_len == 0xd4 + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 + m.read() # read rest of message + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 1 + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 + m.close() + +def test_read_message_in_chunks2(DeviceIndMsg): + m = MemoryStream(DeviceIndMsg, (4,10,0)) + m.read() # read 4 bytes, header incomplere + assert not m.header_valid + assert m.msg_count == 0 + m.read() # read 6 more bytes, header incomplere + assert not m.header_valid + assert m.msg_count == 0 + m.read() # read rest of message + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.header_len==11 + assert m.snr == 2070233889 + assert m.unique_id == '2070233889' + assert m.control == 0x4110 + assert m.serial == 0x0100 + assert m.data_len == 0xd4 + assert m.msg_count == 1 + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 + while m.read(): # read rest of message + pass + assert m.msg_count == 1 + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 + m.close()