From 32ab49b5663dc8635689a76c2e57dc536c3d2afc Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 14 Apr 2024 12:22:25 +0200 Subject: [PATCH 01/37] make depency check in reg_clr_at_midnight optional --- app/src/infos.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/infos.py b/app/src/infos.py index 5fa0697..595e8a7 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -497,7 +497,8 @@ class Infos: keys = row['name'] self.update_db(keys, False, value) - def reg_clr_at_midnight(self, prfx: str) -> None: + def reg_clr_at_midnight(self, prfx: str, + check_dependencies: bool = True) -> None: '''register all registers for the 'ClrAtMidnight' class and check if device of every register is available otherwise ignore the register. @@ -505,7 +506,7 @@ class Infos: prfx:str ==> prefix for the home assistant 'stat_t string'' ''' for id, row in self.info_defs.items(): - if 'ha' in row: + if check_dependencies and 'ha' in row: ha = row['ha'] if 'dev' in ha: device = self.info_devs[ha['dev']] From 57bbd986b333c246542568350677de4a89d112b5 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 14 Apr 2024 12:28:34 +0200 Subject: [PATCH 02/37] register all counters which should be reset at midnight --- app/src/inverter.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/src/inverter.py b/app/src/inverter.py index 41eb39c..9cbe8b2 100644 --- a/app/src/inverter.py +++ b/app/src/inverter.py @@ -26,6 +26,21 @@ class Inverter(): # call Mqtt singleton to establisch the connection to the mqtt broker cls.mqtt = Mqtt(cls._cb_mqtt_is_up) + # register all counters which should be reset at midnight. + # This is needed if the proxy is restated before midnight + # and the inverters are offline, cause the normal refgistering + # needs an update on the counters. + # Without this registration here the counters would not be + # reset at midnight when you restart the proxy just before + # midnight! + inverters = Config.get('inverters') + # logger.debug(f'Inverters: {inverters}') + for inv in inverters.values(): + if (type(inv) is dict): + node_id = inv['node_id'] + cls.db_stat.reg_clr_at_midnight(f'{cls.entity_prfx}{node_id}', + check_dependencies=False) + @classmethod async def _cb_mqtt_is_up(cls) -> None: logging.info('Initialize proxy device on home assistant') From 05b576b1983134e29e50394ca3cf4bc0d6b260eb Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 14 Apr 2024 12:29:27 +0200 Subject: [PATCH 03/37] make code more clear --- app/src/gen3/inverter_g3.py | 2 +- app/src/gen3plus/inverter_g3p.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/gen3/inverter_g3.py b/app/src/gen3/inverter_g3.py index f9a737e..5fccbc2 100644 --- a/app/src/gen3/inverter_g3.py +++ b/app/src/gen3/inverter_g3.py @@ -116,7 +116,7 @@ class InverterG3(Inverter, ConnectionG3): await self.mqtt.publish(f"{self.discovery_prfx}{component}" f"/{node_id}{id}/config", data_json) - self.db.reg_clr_at_midnight(f'{self.entity_prfx}{node_id}') + self.db.reg_clr_at_midnight(f'{self.entity_prfx}{self.node_id}') def close(self) -> None: logging.debug(f'InverterG3.close() l{self.l_addr} | r{self.r_addr}') diff --git a/app/src/gen3plus/inverter_g3p.py b/app/src/gen3plus/inverter_g3p.py index 75f69af..b7b9800 100644 --- a/app/src/gen3plus/inverter_g3p.py +++ b/app/src/gen3plus/inverter_g3p.py @@ -116,7 +116,7 @@ class InverterG3P(Inverter, ConnectionG3P): await self.mqtt.publish(f"{self.discovery_prfx}{component}" f"/{node_id}{id}/config", data_json) - self.db.reg_clr_at_midnight(f'{self.entity_prfx}{node_id}') + self.db.reg_clr_at_midnight(f'{self.entity_prfx}{self.node_id}') def close(self) -> None: logging.debug(f'InverterG3P.close() l{self.l_addr} | r{self.r_addr}') From ac0bf2f8f8cc97c0140317e16f798ed76cded4a5 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 14 Apr 2024 12:30:07 +0200 Subject: [PATCH 04/37] add more unittests for solarman_v5.py --- app/src/gen3plus/solarman_v5.py | 6 +- app/src/messages.py | 2 +- app/tests/test_solarman.py | 149 ++++++++++++++++++++++++++++++-- 3 files changed, 144 insertions(+), 13 deletions(-) diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index 5c8e588..fe1e347 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -186,12 +186,12 @@ class SolarmanV5(Message): type += 'S' return switch.get(type, '???') - def _timestamp(self): # pragma: no cover + def _timestamp(self): # utc as epoche - return int(time.time()) + return int(time.time()) # pragma: no cover def _heartbeat(self) -> int: - return 60 + return 60 # pragma: no cover def __parse_header(self, buf: bytes, buf_len: int) -> None: diff --git a/app/src/messages.py b/app/src/messages.py index 9f6efdf..5bcf711 100644 --- a/app/src/messages.py +++ b/app/src/messages.py @@ -76,7 +76,7 @@ class Message(metaclass=IterRegistry): def _update_header(self, _forward_buffer): '''callback for updating the header of the forward buffer''' - return + return # pragma: no cover ''' Our puplic methods diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py index 28af3ea..0f54f0c 100644 --- a/app/tests/test_solarman.py +++ b/app/tests/test_solarman.py @@ -325,6 +325,54 @@ def UnknownMsg(): # 0x5110 msg += b'\x15' return msg +@pytest.fixture +def SyncStartIndMsg(): # 0x4310 + msg = b'\xa5\x2f\x00\x10\x43\x0c\x0d' +get_sn() +b'\x81\x7a\x0b\x2e\x32' + msg += b'\x39\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x41\x6c\x6c\x69\x75\x73' + msg += b'\x2d\x48\x6f\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x61\x01' + msg += correct_checksum(msg) + msg += b'\x15' + return msg + +@pytest.fixture +def SyncStartRspMsg(): # 0x1310 + msg = b'\xa5\x0a\x00\x10\x13\x0d\x0d' +get_sn() +b'\x81\x01' + msg += total() + msg += hb() + msg += correct_checksum(msg) + msg += b'\x15' + return msg + +@pytest.fixture +def SyncStartFwdMsg(): # 0x4310 + msg = b'\xa5\x2f\x00\x10\x43\x0e\x0d' +get_sn() +b'\x81\x7a\x0b\x2e\x32' + msg += b'\x39\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x41\x6c\x6c\x69\x75\x73' + msg += b'\x2d\x48\x6f\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x61\x01' + msg += correct_checksum(msg) + msg += b'\x15' + return msg + + +@pytest.fixture +def AtCommandIndMsg(): # 0x4510 + msg = b'\xa5\x27\x00\x10\x45\x02\x01' +get_sn() +b'\x01\x02\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'AT+TIME=214028,1,60,120\r' + msg += correct_checksum(msg) + msg += b'\x15' + return msg + +@pytest.fixture +def AtCommandRspMsg(): # 0x1510 + msg = b'\xa5\x0a\x00\x10\x15\x02\x02' +get_sn() +b'\x01\x01' + msg += total() + msg += hb() + msg += correct_checksum(msg) + msg += b'\x15' + return msg + @pytest.fixture def HeartbeatIndMsg(): # 0x4710 msg = b'\xa5\x01\x00\x10\x47\x10\x84' +get_sn() @@ -343,17 +391,19 @@ def HeartbeatRspMsg(): # 0x1710 return msg @pytest.fixture -def AtCommandIndMsg(): # 0x4510 - msg = b'\xa5\x27\x00\x10\x45\x02\x01' +get_sn() +b'\x01\x02\x00' - msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - msg += b'AT+TIME=214028,1,60,120\r' +def SyncEndIndMsg(): # 0x4810 + msg = b'\xa5\x3c\x00\x10\x48\x06\x07' +get_sn() +b'\x01\xa5\x3c\x2e\x32' + msg += b'\x2c\x00\x00\x00\xc1\x01\xec\x33\x01\x05\x2c\xff\xff\xff\xff\xff' + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' + msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' + msg += b'\xff\xff\xff\xff\xff\xff\xff' msg += correct_checksum(msg) msg += b'\x15' return msg @pytest.fixture -def AtCommandRspMsg(): # 0x1510 - msg = b'\xa5\x0a\x00\x10\x15\x03\x01' +get_sn() +b'\x01\x01' +def SyncEndRspMsg(): # 0x1810 + msg = b'\xa5\x0a\x00\x10\x18\x07\x07' +get_sn() +b'\x01\x01' msg += total() msg += hb() msg += correct_checksum(msg) @@ -733,9 +783,86 @@ def test_heartbeat_rsp(ConfigTsunInv1, HeartbeatRspMsg): assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 m.close() +def test_sync_start_ind(ConfigTsunInv1, SyncStartIndMsg, SyncStartRspMsg, SyncStartFwdMsg): + ConfigTsunInv1 + m = MemoryStream(SyncStartIndMsg, (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.header_len==11 + assert m.snr == 2070233889 + # assert m.unique_id == '2070233889' + assert m.control == 0x4310 + assert str(m.seq) == '0d:0d' # value after sending response + assert m.data_len == 47 + assert m._recv_buffer==b'' + assert m._send_buffer==SyncStartRspMsg + assert m._forward_buffer==SyncStartIndMsg + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 + + m._update_header(m._forward_buffer) + assert str(m.seq) == '0d:0e' # value after forwarding indication + assert m._forward_buffer==SyncStartFwdMsg + + m.close() + +def test_sync_start_rsp(ConfigTsunInv1, SyncStartRspMsg): + ConfigTsunInv1 + m = MemoryStream(SyncStartRspMsg, (0,), False) + 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 == 0x1310 + assert str(m.seq) == '0d:0d' # value after sending response + assert m.data_len == 0x0a + assert m._recv_buffer==b'' + assert m._send_buffer==b'' + assert m._forward_buffer==b'' # HeartbeatRspMsg + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 + m.close() + +def test_sync_end_ind(ConfigTsunInv1, SyncEndIndMsg, SyncEndRspMsg): + ConfigTsunInv1 + m = MemoryStream(SyncEndIndMsg, (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.header_len==11 + assert m.snr == 2070233889 + # assert m.unique_id == '2070233889' + assert m.control == 0x4810 + assert str(m.seq) == '07:07' # value after sending response + assert m.data_len == 60 + assert m._recv_buffer==b'' + assert m._send_buffer==SyncEndRspMsg + assert m._forward_buffer==SyncEndIndMsg + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 + m.close() + +def test_sync_end_rsp(ConfigTsunInv1, SyncEndRspMsg): + ConfigTsunInv1 + m = MemoryStream(SyncEndRspMsg, (0,), False) + 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 == 0x1810 + assert str(m.seq) == '07:07' # value after sending response + assert m.data_len == 0x0a + assert m._recv_buffer==b'' + assert m._send_buffer==b'' + assert m._forward_buffer==b'' # HeartbeatRspMsg + assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 + m.close() + def test_at_command_ind(ConfigTsunInv1, AtCommandIndMsg, AtCommandRspMsg): ConfigTsunInv1 - m = MemoryStream(AtCommandIndMsg, (0,)) + m = MemoryStream(AtCommandIndMsg, (0,), False) 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 @@ -743,7 +870,7 @@ def test_at_command_ind(ConfigTsunInv1, AtCommandIndMsg, AtCommandRspMsg): assert m.snr == 2070233889 # assert m.unique_id == '2070233889' assert m.control == 0x4510 - assert str(m.seq) == '01:03' + assert str(m.seq) == '02:02' assert m.data_len == 39 assert m._recv_buffer==b'' assert m._send_buffer==AtCommandRspMsg @@ -817,15 +944,19 @@ def test_build_logger_modell(ConfigTsunAllowAll, DeviceIndMsg): def test_AT_cmd(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, AtCommandIndMsg): ConfigTsunAllowAll - m = MemoryStream(DeviceIndMsg) + m = MemoryStream(DeviceIndMsg, (0,), True) m.read() + assert m.control == 0x4110 + assert str(m.seq) == '01:01' assert m._recv_buffer==b'' assert m._send_buffer==DeviceRspMsg assert m._forward_buffer==DeviceIndMsg + m._send_buffer = bytearray(0) # clear send buffer for next test m._forward_buffer = bytearray(0) # clear send buffer for next test m.send_at_cmd('AT+TIME=214028,1,60,120') assert m._recv_buffer==b'' assert m._send_buffer==AtCommandIndMsg assert m._forward_buffer==b'' + assert str(m.seq) == '01:02' m.close() From 31e049630db95e74f189f7785e6f634bf42d3dbc Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 14 Apr 2024 12:30:58 +0200 Subject: [PATCH 05/37] update changelog --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 100edbd..181c32d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- add experimental handler for `ÀT` commands +- implement self-sufficient island support for GEN3PLUS inverters + see: [#42](https://github.com/s-allius/tsun-gen3-proxy/issues/42) +- Improve error messages on config errors + see: [#46](https://github.com/s-allius/tsun-gen3-proxy/issues/46) - Prepare support of inverters with 6 MTPPs - Clear `Daily Generation` values at midnigth + see: [#32](https://github.com/s-allius/tsun-gen3-proxy/issues/32) - Read pv module details from config file and use it for the Home Assistant registration see: [#43](https://github.com/s-allius/tsun-gen3-proxy/issues/43) -- migrate to aiomqtt version 2.0.0 +- migrate to aiomqtt version 2.0.0 + see: [#44](https://github.com/s-allius/tsun-gen3-proxy/issues/44) ## [0.6.0] - 2024-04-02 From f6af744864188ad03dc17370811176f28e92019a Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 14 Apr 2024 12:31:48 +0200 Subject: [PATCH 06/37] fix flake warning --- app/src/inverter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/inverter.py b/app/src/inverter.py index 9cbe8b2..cf20c96 100644 --- a/app/src/inverter.py +++ b/app/src/inverter.py @@ -31,7 +31,7 @@ class Inverter(): # and the inverters are offline, cause the normal refgistering # needs an update on the counters. # Without this registration here the counters would not be - # reset at midnight when you restart the proxy just before + # reset at midnight when you restart the proxy just before # midnight! inverters = Config.get('inverters') # logger.debug(f'Inverters: {inverters}') From 0e63c45302a6e5f3344ae89f25b6ed11be88f362 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 14 Apr 2024 14:24:32 +0200 Subject: [PATCH 07/37] improve parse() --- app/src/gen3plus/infos_g3p.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index c92bdb2..5d3c6b7 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -96,17 +96,18 @@ class InfosG3P(Infos): mtype = (idx >> 24) & 0xff if ftype != rcv_ftype or mtype != msg_type: continue - if isinstance(row, dict): - info_id = row['reg'] - fmt = row['fmt'] - res = struct.unpack_from(fmt, buf, addr) - result = res[0] - if isinstance(result, (bytearray, bytes)): - result = result.decode('utf-8') - if 'eval' in row: - result = eval(row['eval']) - if 'ratio' in row: - result = round(result * row['ratio'], 2) + if not isinstance(row, dict): + continue + info_id = row['reg'] + fmt = row['fmt'] + res = struct.unpack_from(fmt, buf, addr) + result = res[0] + if isinstance(result, (bytearray, bytes)): + result = result.decode('utf-8') + if 'eval' in row: + result = eval(row['eval']) + if 'ratio' in row: + result = round(result * row['ratio'], 2) keys, level, unit, must_incr = self._key_obj(info_id) From c1e114447ad6e06423c37b08a958154a9e4de7f7 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 14 Apr 2024 14:39:01 +0200 Subject: [PATCH 08/37] rename unit test files for GEN3 --- app/tests/{test_infos.py => test_infos_g3.py} | 0 app/tests/{test_messages.py => test_talent.py} | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) rename app/tests/{test_infos.py => test_infos_g3.py} (100%) rename app/tests/{test_messages.py => test_talent.py} (99%) diff --git a/app/tests/test_infos.py b/app/tests/test_infos_g3.py similarity index 100% rename from app/tests/test_infos.py rename to app/tests/test_infos_g3.py diff --git a/app/tests/test_messages.py b/app/tests/test_talent.py similarity index 99% rename from app/tests/test_messages.py rename to app/tests/test_talent.py index 0b75dbe..89fd420 100644 --- a/app/tests/test_messages.py +++ b/app/tests/test_talent.py @@ -679,7 +679,7 @@ def test_msg_ota_invalid(ConfigTsunInv1, MsgOtaInvalid): def test_msg_unknown(ConfigTsunInv1, MsgUnknown): ConfigTsunInv1 m = MemoryStream(MsgUnknown, (0,), False) - m.db.stat['proxy']['Unknown_Ctrl'] = 0 + m.db.stat['proxy']['Unknown_Msg'] = 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 @@ -716,6 +716,8 @@ def test_msg_iterator(): test1+=1 elif key == m2: test2+=1 + elif type(key) != Talent: + continue else: assert False assert test1 == 1 From 2ade04e6ccea508e9e5a0b787067c18d7982495d Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 14 Apr 2024 16:01:30 +0200 Subject: [PATCH 09/37] move common info tests form test_infos_g3 to test_infos --- app/tests/test_infos.py | 186 +++++++++++++++++++++++++++++++++++++ app/tests/test_infos_g3.py | 186 +------------------------------------ 2 files changed, 187 insertions(+), 185 deletions(-) create mode 100644 app/tests/test_infos.py diff --git a/app/tests/test_infos.py b/app/tests/test_infos.py new file mode 100644 index 0000000..aa0b3fd --- /dev/null +++ b/app/tests/test_infos.py @@ -0,0 +1,186 @@ +# test_with_pytest.py +import pytest, json +from app.src.infos import Register, ClrAtMidnight +from app.src.infos import Infos + +def test_statistic_counter(): + i = Infos() + val = i.dev_value("Test-String") + assert val == "Test-String" + + val = i.dev_value(0xffffffff) # invalid addr + assert val == None + + val = i.dev_value(Register.INVERTER_CNT) # valid addr but not initiliazed + 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, "Invalid_Msg_Format": 0, "AT_Command": 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, "Invalid_Msg_Format": 0, "AT_Command": 0}}) + val = i.dev_value(Register.INVERTER_CNT) + assert val == 1 + + i.dec_counter('Inverter_Cnt') + val = i.dev_value(Register.INVERTER_CNT) + assert val == 0 + +def test_dep_rules(): + i = Infos() + i.static_init() # initialize counter + + res = i.ignore_this_device({}) + assert res == True + + res = i.ignore_this_device({'reg':0xffffffff}) + assert res == True + + i.inc_counter('Inverter_Cnt') # is 1 + val = i.dev_value(Register.INVERTER_CNT) + assert val == 1 + res = i.ignore_this_device({'reg': Register.INVERTER_CNT}) + assert res == True + res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'less_eq': 2}) + assert res == False + res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'gte': 2}) + assert res == True + + i.inc_counter('Inverter_Cnt') # is 2 + res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'less_eq': 2}) + assert res == False + res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'gte': 2}) + assert res == False + + i.inc_counter('Inverter_Cnt') # is 3 + res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'less_eq': 2}) + assert res == True + res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'gte': 2}) + assert res == False + +def test_table_definition(): + i = Infos() + i.static_init() # initialize counter + + val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter + assert val == 0 + + # for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'): + # pass + for reg in Register: + i.ha_conf(reg, ha_prfx="tsun/", node_id="garagendach/", snr='123', singleton=False, sug_area = 'roof') # noqa: E501 + + + for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'): + pass + + val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter + assert val == 0 + + # test missing 'fmt' value + i.info_defs[Register.TEST_REG1] = {'name':['proxy', 'Internal_Test1'], 'singleton': True, 'ha':{'dev':'proxy', 'dev_cla': None, 'stat_cla': None, 'id':'intern_test1_'}} + + tests = 0 + for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'): + if id == 'intern_test1_456': + tests +=1 + + assert tests == 1 + + val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter + assert val == 1 + + # test missing 'dev' value + i.info_defs[Register.TEST_REG1] = {'name':['proxy', 'Internal_Test2'], 'singleton': True, 'ha':{'dev_cla': None, 'stat_cla': None, 'id':'intern_test2_', 'fmt':'| int'}} + tests = 0 + for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'): + if id == 'intern_test2_456': + tests +=1 + + assert tests == 1 + + val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter + assert val == 2 + + + + # test invalid 'via' value + i.info_devs['test_dev'] = {'via':'xyz', 'name':'Module PV1'} + + i.info_defs[Register.TEST_REG1] = {'name':['proxy', 'Internal_Test2'], 'singleton': True, 'ha':{'dev':'test_dev', 'dev_cla': None, 'stat_cla': None, 'id':'intern_test2_', 'fmt':'| int'}} + tests = 0 + for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'): + if id == 'intern_test2_456': + tests +=1 + + assert tests == 1 + + val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter + assert val == 3 + +def test_clr_at_midnight(): + i = Infos() + i.static_init() # initialize counter + i.set_db_def_value(Register.NO_INPUTS, 2) + val = i.dev_value(Register.NO_INPUTS) # valid addr but not initiliazed + assert val == 2 + i.info_defs[Register.TEST_REG1] = { # add a entry with incomplete ha definition + 'name': ['test', 'grp', 'REG_1'], 'ha': {'dev_cla': None } + } + i.reg_clr_at_midnight('tsun/inv_1/') + # tsun/inv_2/input + assert json.dumps(ClrAtMidnight.db['tsun/inv_1/total']) == json.dumps({'Daily_Generation': 0}) + assert json.dumps(ClrAtMidnight.db['tsun/inv_1/input']) == json.dumps({"pv1": {"Daily_Generation": 0}, "pv2": {"Daily_Generation": 0}}) + + i.reg_clr_at_midnight('tsun/inv_1/') + assert json.dumps(ClrAtMidnight.db['tsun/inv_1/total']) == json.dumps({'Daily_Generation': 0}) + assert json.dumps(ClrAtMidnight.db['tsun/inv_1/input']) == json.dumps({"pv1": {"Daily_Generation": 0}, "pv2": {"Daily_Generation": 0}}) + + test = 0 + for key, data in ClrAtMidnight.elm(): + if key == 'tsun/inv_1/total': + assert json.dumps(data) == json.dumps({'Daily_Generation': 0}) + test += 1 + elif key == 'tsun/inv_1/input': + assert json.dumps(data) == json.dumps({"pv1": {"Daily_Generation": 0}, "pv2": {"Daily_Generation": 0}}) + test += 1 + assert test == 2 + assert json.dumps(ClrAtMidnight.db) == json.dumps({}) + + i.reg_clr_at_midnight('tsun/inv_1/') + +def test_pv_module_config(): + i = Infos() + # i.set_db_def_value(Register.NO_INPUTS, 2) + + dt = { + 'pv1':{'manufacturer':'TSUN1','type': 'Module 100W'}, + 'pv2':{'manufacturer':'TSUN2'}, + 'pv3':{'manufacturer':'TSUN3','type': 'Module 300W'}, + 'pv4':{'type': 'Module 400W'}, + 'pv5':{}, + } + i.set_pv_module_details(dt) + assert 'TSUN1' == i.dev_value(Register.PV1_MANUFACTURER) + assert 'TSUN2' == i.dev_value(Register.PV2_MANUFACTURER) + assert 'TSUN3' == i.dev_value(Register.PV3_MANUFACTURER) + assert None == i.dev_value(Register.PV4_MANUFACTURER) + assert None == i.dev_value(Register.PV5_MANUFACTURER) + assert 'Module 100W' == i.dev_value(Register.PV1_MODEL) + assert None == i.dev_value(Register.PV2_MODEL) + assert 'Module 300W' == i.dev_value(Register.PV3_MODEL) + assert 'Module 400W' == i.dev_value(Register.PV4_MODEL) + assert None == i.dev_value(Register.PV5_MODEL) + +def test_broken_info_defs(): + i = Infos() + val = i.get_db_value(Register.NO_INPUTS, 666) + assert val == 666 + i.info_defs[Register.TEST_REG1] = 'test' # add a string instead of a dict + val = i.get_db_value(Register.TEST_REG1, 666) + assert val == 666 + i.set_db_def_value(Register.TEST_REG1, 2) + del i.info_defs[Register.TEST_REG1] # delete the broken entry diff --git a/app/tests/test_infos_g3.py b/app/tests/test_infos_g3.py index 8bf419d..193536e 100644 --- a/app/tests/test_infos_g3.py +++ b/app/tests/test_infos_g3.py @@ -390,124 +390,7 @@ def test_must_incr_total2(InvDataSeq2, InvDataSeq2_Zero): assert tests==3 assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36}) assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}}) - assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23}) - - -def test_statistic_counter(): - i = InfosG3() - val = i.dev_value("Test-String") - assert val == "Test-String" - - val = i.dev_value(0xffffffff) # invalid addr - assert val == None - - val = i.dev_value(Register.INVERTER_CNT) # valid addr but not initiliazed - 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, "Invalid_Msg_Format": 0, "AT_Command": 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, "Invalid_Msg_Format": 0, "AT_Command": 0}}) - val = i.dev_value(Register.INVERTER_CNT) - assert val == 1 - - i.dec_counter('Inverter_Cnt') - val = i.dev_value(Register.INVERTER_CNT) - assert val == 0 - -def test_dep_rules(): - i = InfosG3() - i.static_init() # initialize counter - - res = i.ignore_this_device({}) - assert res == True - - res = i.ignore_this_device({'reg':0xffffffff}) - assert res == True - - i.inc_counter('Inverter_Cnt') # is 1 - val = i.dev_value(Register.INVERTER_CNT) - assert val == 1 - res = i.ignore_this_device({'reg': Register.INVERTER_CNT}) - assert res == True - res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'less_eq': 2}) - assert res == False - res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'gte': 2}) - assert res == True - - i.inc_counter('Inverter_Cnt') # is 2 - res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'less_eq': 2}) - assert res == False - res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'gte': 2}) - assert res == False - - i.inc_counter('Inverter_Cnt') # is 3 - res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'less_eq': 2}) - assert res == True - res = i.ignore_this_device({'reg': Register.INVERTER_CNT, 'gte': 2}) - assert res == False - -def test_table_definition(): - i = InfosG3() - i.static_init() # initialize counter - - val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter - assert val == 0 - - for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'): - pass - - for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'): - pass - - val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter - assert val == 0 - - # test missing 'fmt' value - i.info_defs[Register.TEST_REG1] = {'name':['proxy', 'Internal_Test1'], 'singleton': True, 'ha':{'dev':'proxy', 'dev_cla': None, 'stat_cla': None, 'id':'intern_test1_'}} - - tests = 0 - for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'): - if id == 'intern_test1_456': - tests +=1 - - assert tests == 1 - - val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter - assert val == 1 - - # test missing 'dev' value - i.info_defs[Register.TEST_REG1] = {'name':['proxy', 'Internal_Test2'], 'singleton': True, 'ha':{'dev_cla': None, 'stat_cla': None, 'id':'intern_test2_', 'fmt':'| int'}} - tests = 0 - for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'): - if id == 'intern_test2_456': - tests +=1 - - assert tests == 1 - - val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter - assert val == 2 - - - - # test invalid 'via' value - i.info_devs['test_dev'] = {'via':'xyz', 'name':'Module PV1'} - - i.info_defs[Register.TEST_REG1] = {'name':['proxy', 'Internal_Test2'], 'singleton': True, 'ha':{'dev':'test_dev', 'dev_cla': None, 'stat_cla': None, 'id':'intern_test2_', 'fmt':'| int'}} - tests = 0 - for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'): - if id == 'intern_test2_456': - tests +=1 - - assert tests == 1 - - val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter - assert val == 3 - + def test_invalid_data_type(InvalidDataSeq): i = InfosG3() @@ -523,70 +406,3 @@ def test_invalid_data_type(InvalidDataSeq): val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter assert val == 1 - -def test_clr_at_midnight(): - i = InfosG3() - i.static_init() # initialize counter - i.set_db_def_value(Register.NO_INPUTS, 2) - val = i.dev_value(Register.NO_INPUTS) # valid addr but not initiliazed - assert val == 2 - i.info_defs[Register.TEST_REG1] = { # add a entry with incomplete ha definition - 'name': ['test', 'grp', 'REG_1'], 'ha': {'dev_cla': None } - } - i.reg_clr_at_midnight('tsun/inv_1/') - # tsun/inv_2/input - assert json.dumps(ClrAtMidnight.db['tsun/inv_1/total']) == json.dumps({'Daily_Generation': 0}) - assert json.dumps(ClrAtMidnight.db['tsun/inv_1/input']) == json.dumps({"pv1": {"Daily_Generation": 0}, "pv2": {"Daily_Generation": 0}}) - - i.reg_clr_at_midnight('tsun/inv_1/') - assert json.dumps(ClrAtMidnight.db['tsun/inv_1/total']) == json.dumps({'Daily_Generation': 0}) - assert json.dumps(ClrAtMidnight.db['tsun/inv_1/input']) == json.dumps({"pv1": {"Daily_Generation": 0}, "pv2": {"Daily_Generation": 0}}) - - test = 0 - for key, data in ClrAtMidnight.elm(): - if key == 'tsun/inv_1/total': - assert json.dumps(data) == json.dumps({'Daily_Generation': 0}) - test += 1 - elif key == 'tsun/inv_1/input': - assert json.dumps(data) == json.dumps({"pv1": {"Daily_Generation": 0}, "pv2": {"Daily_Generation": 0}}) - test += 1 - assert test == 2 - assert json.dumps(ClrAtMidnight.db) == json.dumps({}) - - i.reg_clr_at_midnight('tsun/inv_1/') - - - - -def test_pv_module_config(): - i = InfosG3() - # i.set_db_def_value(Register.NO_INPUTS, 2) - - dt = { - 'pv1':{'manufacturer':'TSUN1','type': 'Module 100W'}, - 'pv2':{'manufacturer':'TSUN2'}, - 'pv3':{'manufacturer':'TSUN3','type': 'Module 300W'}, - 'pv4':{'type': 'Module 400W'}, - 'pv5':{}, - } - i.set_pv_module_details(dt) - assert 'TSUN1' == i.dev_value(Register.PV1_MANUFACTURER) - assert 'TSUN2' == i.dev_value(Register.PV2_MANUFACTURER) - assert 'TSUN3' == i.dev_value(Register.PV3_MANUFACTURER) - assert None == i.dev_value(Register.PV4_MANUFACTURER) - assert None == i.dev_value(Register.PV5_MANUFACTURER) - assert 'Module 100W' == i.dev_value(Register.PV1_MODEL) - assert None == i.dev_value(Register.PV2_MODEL) - assert 'Module 300W' == i.dev_value(Register.PV3_MODEL) - assert 'Module 400W' == i.dev_value(Register.PV4_MODEL) - assert None == i.dev_value(Register.PV5_MODEL) - -def test_broken_info_defs(): - i = InfosG3() - val = i.get_db_value(Register.NO_INPUTS, 666) - assert val == 666 - i.info_defs[Register.TEST_REG1] = 'test' # add a string instead of a dict - val = i.get_db_value(Register.TEST_REG1, 666) - assert val == 666 - i.set_db_def_value(Register.TEST_REG1, 2) - From f4aa7004e55d64385a594373ab093383b2c0ea93 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 14 Apr 2024 17:52:02 +0200 Subject: [PATCH 10/37] increase test coverage for infos.py by to 100% --- app/tests/test_infos.py | 50 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/app/tests/test_infos.py b/app/tests/test_infos.py index aa0b3fd..d3b542e 100644 --- a/app/tests/test_infos.py +++ b/app/tests/test_infos.py @@ -1,5 +1,7 @@ # test_with_pytest.py -import pytest, json +import pytest +import json +import logging from app.src.infos import Register, ClrAtMidnight from app.src.infos import Infos @@ -184,3 +186,49 @@ def test_broken_info_defs(): assert val == 666 i.set_db_def_value(Register.TEST_REG1, 2) del i.info_defs[Register.TEST_REG1] # delete the broken entry + +def test_get_value(): + i = Infos() + assert None == i.get_db_value(Register.PV1_VOLTAGE, None) + assert None == i.get_db_value(Register.PV2_VOLTAGE, None) + + i.set_db_def_value(Register.PV1_VOLTAGE, 30) + assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None) + assert None == i.get_db_value(Register.PV2_VOLTAGE, None) + + i.set_db_def_value(Register.PV2_VOLTAGE, 30.3) + assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None) + assert 30.3 == i.get_db_value(Register.PV2_VOLTAGE, None) + +def test_update_value(): + i = Infos() + assert None == i.get_db_value(Register.PV1_VOLTAGE, None) + + keys = i.info_defs[Register.PV1_VOLTAGE]['name'] + name, update = i.update_db(keys, True, 30) + assert update == True + assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None) + + keys = i.info_defs[Register.PV1_VOLTAGE]['name'] + name, update = i.update_db(keys, True, 30) + assert update == False + assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None) + + keys = i.info_defs[Register.PV1_VOLTAGE]['name'] + name, update = i.update_db(keys, False, 29) + assert update == True + assert 29 == i.get_db_value(Register.PV1_VOLTAGE, None) + +def test_key_obj(): + i = Infos() + keys, level, unit, must_incr = i._key_obj(Register.PV1_VOLTAGE) + assert keys == ['input', 'pv1', 'Voltage'] + assert level == logging.DEBUG + assert unit == 'V' + assert must_incr == False + + keys, level, unit, must_incr = i._key_obj(Register.PV1_DAILY_GENERATION) + assert keys == ['input', 'pv1', 'Daily_Generation'] + assert level == logging.DEBUG + assert unit == 'kWh' + assert must_incr == True From 64362dad210aee892103759d71da54f768f2e2ad Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 14 Apr 2024 20:36:20 +0200 Subject: [PATCH 11/37] remove trailing '\x00' from received strings --- app/src/gen3plus/infos_g3p.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index 5d3c6b7..e3a15dd 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -103,7 +103,7 @@ class InfosG3P(Infos): res = struct.unpack_from(fmt, buf, addr) result = res[0] if isinstance(result, (bytearray, bytes)): - result = result.decode('utf-8') + result = result.decode().split('\x00')[0] if 'eval' in row: result = eval(row['eval']) if 'ratio' in row: From 19c143d894d477bfa9dd2032742aeffc83143eb7 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 14 Apr 2024 20:38:16 +0200 Subject: [PATCH 12/37] unittest for Infos_G3P class --- app/tests/test_infos_g3p.py | 151 ++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 app/tests/test_infos_g3p.py diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py new file mode 100644 index 0000000..ddaef0b --- /dev/null +++ b/app/tests/test_infos_g3p.py @@ -0,0 +1,151 @@ + +# test_with_pytest.py +from typing import Literal +import pytest, json +from app.src.infos import Register, ClrAtMidnight +from app.src.gen3plus.infos_g3p import InfosG3P + +@pytest.fixture +def DeviceData(): # 0x4110 ftype: 0x02 + msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\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' + return msg + +@pytest.fixture +def InverterData(): # 0x4210 ftype: 0x01 + msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xb0\x02\xbc\xc8' + msg += b'\x24\x32\x6c\x1f\x00\x00\xa0\x47\xe4\x33\x01\x00\x03\x08\x00\x00' + msg += b'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45' + 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\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\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\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\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x40\x10\x08\xc8\x00\x49\x13\x8d\x00\x36\x00\x00\x02\x58\x06\x7a' + msg += b'\x01\x61\x00\xa8\x02\x54\x01\x5a\x00\x8a\x01\xe4\x01\x5a\x00\xbd' + msg += b'\x02\x8f\x00\x11\x00\x01\x00\x00\x00\x0b\x00\x00\x27\x98\x00\x04' + msg += b'\x00\x00\x0c\x04\x00\x03\x00\x00\x0a\xe7\x00\x05\x00\x00\x0c\x75' + msg += b'\x00\x00\x00\x00\x06\x16\x02\x00\x00\x00\x55\xaa\x00\x01\x00\x00' + msg += b'\x00\x00\x00\x00\xff\xff\x07\xd0\x00\x03\x04\x00\x04\x00\x04\x00' + msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00' + msg += b'\x09\xcd\x07\xb6\x13\x9c\x13\x24\x00\x01\x07\xae\x04\x0f\x00\x41' + msg += b'\x00\x0f\x0a\x64\x0a\x64\x00\x06\x00\x06\x09\xf6\x12\x8c\x12\x8c' + msg += b'\x00\x10\x00\x10\x14\x52\x14\x52\x00\x10\x00\x10\x01\x51\x00\x05' + msg += b'\x04\x00\x00\x01\x13\x9c\x0f\xa0\x00\x4e\x00\x66\x03\xe8\x04\x00' + msg += b'\x09\xce\x07\xa8\x13\x9c\x13\x26\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x04\x00\x04\x00\x00\x00\x00\x00\xff\xff\x00\x00' + msg += b'\x00\x00\x00\x00' + return msg + + +def test_default_db(): + i = InfosG3P() + + assert json.dumps(i.db) == json.dumps({ + "inverter": {"Manufacturer": "TSUN", "Equipment_Model": "TSOL-MSxx00"}, + "collector": {"Chip_Type": "IGEN TECH"}, + }) + +def test_parse_4110(DeviceData: bytes): + i = InfosG3P() + i.db.clear() + for key, update in i.parse (DeviceData, 0x41, 2): + pass + + assert json.dumps(i.db) == json.dumps({ + 'controller': {"Data_Up_Interval": 300, "Collect_Interval": 60, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Adress": "192.168.80.49"}, + 'collector': {"Collector_Fw_Version": "LSW5BLE_17_02B0_1.05"}, + 'inverter': {"Version": "V1.1.00.0B"}, + }) + +def test_parse_4210(InverterData: bytes): + i = InfosG3P() + i.db.clear() + + for key, update in i.parse (InverterData, 0x42, 1): + pass + + assert json.dumps(i.db) == json.dumps({ + "controller": {"Power_On_Time": 776}, + "inverter": {"Serial_Number": "Y17E7A0F010B013E", "Rated_Power": 600, "Max_Designed_Power": 2000, "No_Inputs": 4}, + "grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8}, + "env": {"Inverter_Temp": 54}, + "input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76}, + "pv2": {"Voltage": 34.6, "Current": 1.38, "Power": 48.4, "Daily_Generation": 0.03, "Total_Generation": 27.91}, + "pv3": {"Voltage": 34.6, "Current": 1.89, "Power": 65.5, "Daily_Generation": 0.05, "Total_Generation": 31.89}, + "pv4": {"Voltage": 1.7, "Current": 0.01, "Power": 0.0, "Total_Generation": 15.58}}, + "total": {"Daily_Generation": 0.11, "Total_Generation": 101.36} + }) + +def test_build_ha_conf1(): + i = InfosG3P() + i.static_init() # initialize counter + + tests = 0 + for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'): + + if id == 'out_power_123': + assert comp == 'sensor' + assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/grid", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "out_power_123", "val_tpl": "{{value_json['Output_Power'] | float}}", "unit_of_meas": "W", "dev": {"name": "Micro Inverter", "sa": "Micro Inverter", "via_device": "controller_123", "mdl": "TSOL-MSxx00", "mf": "TSUN", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) + tests +=1 + + elif id == 'daily_gen_123': + assert comp == 'sensor' + assert d_json == json.dumps({"name": "Daily Generation", "stat_t": "tsun/garagendach/total", "dev_cla": "energy", "stat_cla": "total_increasing", "uniq_id": "daily_gen_123", "val_tpl": "{{value_json['Daily_Generation'] | float}}", "unit_of_meas": "kWh", "ic": "mdi:solar-power-variant", "dev": {"name": "Micro Inverter", "sa": "Micro Inverter", "via_device": "controller_123", "mdl": "TSOL-MSxx00", "mf": "TSUN", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) + tests +=1 + + 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", "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 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", "via_device": "proxy", "mf": "IGEN TECH", "ids": ["controller_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) + tests +=1 + elif id == 'inv_count_456': + assert False + + assert tests==4 + + + for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'): + + if id == 'out_power_123': + assert False + elif id == 'daily_gen_123': + assert False + elif id == 'power_pv1_123': + assert False + elif id == 'power_pv2_123': + 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 False + elif id == 'inv_count_456': + assert comp == 'sensor' + assert d_json == json.dumps({"name": "Active Inverter Connections", "stat_t": "tsun/proxy/proxy", "dev_cla": None, "stat_cla": None, "uniq_id": "inv_count_456", "val_tpl": "{{value_json['Inverter_Cnt'] | int}}", "ic": "mdi:counter", "dev": {"name": "Proxy", "sa": "Proxy", "mdl": "proxy", "mf": "Stefan Allius", "sw": "unknown", "ids": ["proxy"]}, "o": {"name": "proxy", "sw": "unknown"}}) + tests +=1 + + assert tests==5 From 9682379bcd245cc37f054a1e08aac334f4fa319e Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 14 Apr 2024 21:02:20 +0200 Subject: [PATCH 13/37] increase test coverage for infos_g3p.py to 100% --- app/tests/test_infos_g3p.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index ddaef0b..da5a63c 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -1,9 +1,9 @@ # test_with_pytest.py -from typing import Literal import pytest, json -from app.src.infos import Register, ClrAtMidnight +from app.src.infos import Register from app.src.gen3plus.infos_g3p import InfosG3P +from app.src.gen3plus.infos_g3p import RegisterMap @pytest.fixture def DeviceData(): # 0x4110 ftype: 0x02 @@ -149,3 +149,29 @@ def test_build_ha_conf1(): tests +=1 assert tests==5 + +def test_exception_and_eval(InverterData: bytes): + + # add eval to convert temperature from °F to °C + RegisterMap.map[0x420100d8]['eval'] = '(result-32)/1.8' + # map PV1_VOLTAGE to invalid register + RegisterMap.map[0x420100e0]['reg'] = Register.TEST_REG2 + # set invalid maping entry for OUTPUT_POWER (string instead of dict type) + Backup = RegisterMap.map[0x420100de] + RegisterMap.map[0x420100de] = 'invalid_entry' + + i = InfosG3P() + # i.db.clear() + + for key, update in i.parse (InverterData, 0x42, 1): + pass + assert 12.2222 == round (i.get_db_value(Register.INVERTER_TEMP, 0),4) + + del RegisterMap.map[0x420100d8]['eval'] # remove eval + RegisterMap.map[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping + RegisterMap.map[0x420100de] = Backup # reset mapping + + for key, update in i.parse (InverterData, 0x42, 1): + pass + assert 54 == i.get_db_value(Register.INVERTER_TEMP, 0) + \ No newline at end of file From b1ea63b00d5a41ea9fa3a987f38387ce806f76af Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 14 Apr 2024 21:29:41 +0200 Subject: [PATCH 14/37] use test serial number to identify the test case --- app/tests/test_infos_g3p.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index da5a63c..293c44d 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -27,7 +27,7 @@ def DeviceData(): # 0x4110 ftype: 0x02 def InverterData(): # 0x4210 ftype: 0x01 msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xb0\x02\xbc\xc8' msg += b'\x24\x32\x6c\x1f\x00\x00\xa0\x47\xe4\x33\x01\x00\x03\x08\x00\x00' - msg += b'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45' + msg += b'\x59\x31\x37\x45\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x45' 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\x00\x00\x00\x00\x00\x00\x00\x00\x00' @@ -84,7 +84,7 @@ def test_parse_4210(InverterData: bytes): assert json.dumps(i.db) == json.dumps({ "controller": {"Power_On_Time": 776}, - "inverter": {"Serial_Number": "Y17E7A0F010B013E", "Rated_Power": 600, "Max_Designed_Power": 2000, "No_Inputs": 4}, + "inverter": {"Serial_Number": "Y17E00000000000E", "Rated_Power": 600, "Max_Designed_Power": 2000, "No_Inputs": 4}, "grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8}, "env": {"Inverter_Temp": 54}, "input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76}, From 3d09d592a6a047516daa173b659c90c9853ea727 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Mon, 15 Apr 2024 00:10:01 +0200 Subject: [PATCH 15/37] add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 181c32d..70a27d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- switch to aiomqtt version 2.0.1 +- refactor unittest and increase testcoverage - add experimental handler for `ÀT` commands - implement self-sufficient island support for GEN3PLUS inverters see: [#42](https://github.com/s-allius/tsun-gen3-proxy/issues/42) From 6eec4b312e7320a1f5f813185f4bc9b268391e1b Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Mon, 15 Apr 2024 00:10:26 +0200 Subject: [PATCH 16/37] switch to aiomqtt version 2.0.1 --- app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/requirements.txt b/app/requirements.txt index 7558187..b151101 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,3 +1,3 @@ - aiomqtt==2.0.0 + aiomqtt==2.0.1 schema==0.7.5 aiocron==1.8 \ No newline at end of file From 1f70bd49c54df7a339f9500df6786b2a521af68f Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Mon, 15 Apr 2024 00:14:25 +0200 Subject: [PATCH 17/37] switch to aiomqtt version 2.0.1 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3552aec..2c24b97 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

License: BSD-3-Clause Supported Python versions - Supported aiomqtt versions + Supported aiomqtt versions Supported aiocron versions Supported toml versions From 44c9b80c7e7b5beea9359aa31f1e4e77fb3a3e25 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Mon, 15 Apr 2024 21:26:48 +0200 Subject: [PATCH 18/37] fix linter warnings --- README.md | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 2c24b97..8d09650 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,8 @@ Supported aiomqtt versions Supported aiocron versions Supported toml versions -

- # Overview This proxy enables a reliable connection between TSUN third generation inverters and an MQTT broker. With the proxy, you can easily retrieve real-time values such as power, current and daily energy and integrate the inverter into typical home automations. This works even without an internet connection. The optional connection to the TSUN Cloud can be disabled! @@ -23,11 +21,14 @@ In detail, the inverter establishes a TCP connection to the TSUN cloud to transm Through this, the inverter then establishes a connection to the proxy and the proxy establishes another connection to the TSUN Cloud. The transmitted data is interpreted by the proxy and then passed on to both the TSUN Cloud and the MQTT broker. The connection to the TSUN Cloud is optional and can be switched off in the configuration (default is on). Then no more data is sent to the Internet, but no more remote updates of firmware and operating parameters (e.g. rated power, grid parameters) are possible. By means of `docker` a simple installation and operation is possible. By using `docker-composer`, a complete stack of proxy, `MQTT-brocker` and `home-assistant` can be started easily. -### -ℹ️ This project is not related to the company TSUN. It is a private initiative that aims to connect TSUN inverters with an MQTT broker. There is no support and no warranty from TSUN. -### -``` +## + +ℹ️ This project is not related to the company TSUN. It is a private initiative that aims to connect TSUN inverters with an MQTT broker. There is no support and no warranty from TSUN. + +## + +```txt ❗An essential requirement is that the proxy can be looped into the connection between the inverter and TSUN Cloud. @@ -51,45 +52,51 @@ If you use a Pi-hole, you can also store the host entry in the Pi-hole. Here are some screenshots of how the inverter is displayed in the Home Assistant: -https://github.com/s-allius/tsun-gen3-proxy/wiki/home-assistant#home-assistant-screenshots + + ## Requirements - A running Docker engine to host the container - Ability to loop the proxy into the connection between the inverter and the TSUN cloud - # Getting Started To run the proxy, you first need to create the image. You can do this quite simply as follows: + ```sh docker build https://github.com/s-allius/tsun-gen3-proxy.git#main:app -t tsun-proxy ``` + after that you can run the image: + ```sh docker run --dns '8.8.8.8' --env 'UID=1000' -p '5005:5005' -p '10000:10000' -v ./config:/home/tsun-proxy/config -v ./log:/home/tsun-proxy/log tsun-proxy ``` -You will surely see a message that the configuration file was not found. So that we can create this without admin rights, the `uid` must still be adapted. To do this, simply stop the proxy with ctrl-c and use the `id` command to determine your own UserId: + +You will surely see a message that the configuration file was not found. So that we can create this without admin rights, the `uid` must still be adapted. To do this, simply stop the proxy with ctrl-c and use the `id` command to determine your own UserId: + ```sh % id uid=1050(sallius) gid=20(staff) ... ``` + With this information we can customize the `docker run`` statement: + ```sh docker run --dns '8.8.8.8' --env 'UID=1050' -p '5005:5005' -p '10000:10000' -v ./config:/home/tsun-proxy/config -v ./log:/home/tsun-proxy/log tsun-proxy ``` # Configuration -The Docker container does not require any special configuration. + +The Docker container does not require any special configuration. On the host, two directories (for log files and for config files) must be mapped. If necessary, the UID of the proxy process can be adjusted, which is also the owner of the log and configuration files. The proxy can be configured via the file 'config.toml'. When the proxy is started, a file 'config.example.toml' is copied into the config directory. This file shows all possible parameters and their default values. Changes in the example file itself are not evaluated. To configure the proxy, the config.example.toml file should be renamed to config.toml. After that the corresponding values can be adjusted. To load the new configuration, the proxy must be restarted. - ## Proxy Configuration + The configration uses the TOML format, which aims to be easy to read due to obvious semantics. -You find more details here: https://toml.io/en/v1.0.0 - - +You find more details here: ```toml # configuration for tsun cloud for 'GEN3' inverters @@ -152,6 +159,7 @@ pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module de ## DNS Settings ### Loop the proxy into the connection + To include the proxy in the connection between the inverter and the TSUN Cloud, you must adapt the DNS record of *logger.talent-monitoring.com* within the network that your inverter uses. You need a mapping from logger.talent-monitoring.com to the IP address of the host running the Docker engine. The new GEN3 PLUS inverters use a different URL. Here, *iot.talent-monitoring.com* must be redirected. @@ -159,21 +167,25 @@ The new GEN3 PLUS inverters use a different URL. Here, *iot.talent-monitoring.co This can be done, for example, by adding a local DNS record to the Pi-hole if you are using it. ### DNS Rebind Protection + If you are using a router as local DNS server, the router may have DNS rebind protection that needs to be adjusted. For security reasons, DNS rebind protection blocks DNS queries that refer to an IP address on the local network. If you are using a FRITZ!Box, you can do this in the Network Settings tab under Home Network / Network. Add logger.talent-monitoring.com as a hostname exception in DNS rebind protection. ### DNS server of proxy + The proxy itself must use a different DNS server to connect to the TSUN Cloud. If you use the DNS server with the adapted record, you will end up in an endless loop as soon as the proxy tries to send data to the TSUN Cloud. As described above, set a DNS sever in the Docker command or Docker compose file. ### Over The Air (OTA) firmware update -Even if the proxy is connected between the inverter and the TSUN Cloud, an OTA update is supported. To do this, the inverter must be able to reach the website http://www.talent-monitoring.com:9002/ in order to download images from there. + +Even if the proxy is connected between the inverter and the TSUN Cloud, an OTA update is supported. To do this, the inverter must be able to reach the website in order to download images from there. It must be ensured that this address is not mapped to the proxy! ## Compatibility + In the following table you will find an overview of which inverter model has been tested for compatibility with which firmware version. A combination with a red question mark should work, but I have not checked it in detail. @@ -185,13 +197,14 @@ A combination with a red question mark should work, but I have not checked it in TITAN micro inverters:
TSOL-MP3000, MP2250, MS3000❓❓❓❓ -``` +```txt Legend ➖: Firmware not available for this devices ✔️: proxy support testet ❓: proxy support possible but not testet 🚧: Proxy support in preparation ``` + ❗The new inverters of the GEN3 Plus generation (e.g. MS-2000) use a completely different protocol for data transmission to the TSUN server. These inverters are supported from proxy version 0.6. The serial numbers of these inverters start with `Y17E` instead of `R17E` If you have one of these combinations with a red question mark, it would be very nice if you could send me a proxy trace so that I can carry out the detailed checks and adjust the device and system tests. [Ask here how to send a trace](https://github.com/s-allius/tsun-gen3-proxy/discussions/categories/traces-for-compatibility-check) @@ -216,4 +229,3 @@ We're very happy to receive contributions to this project! You can get started b ## Changelog The changelog lives in [CHANGELOG.md](https://github.com/s-allius/tsun-gen3-proxy/blob/main/CHANGELOG.md). It follows the principles of [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - From 4c923b0dedede8427e084aff3bc40a3008e56055 Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Mon, 15 Apr 2024 21:33:37 +0200 Subject: [PATCH 19/37] Update README.md --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8d09650..cb85d3e 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,9 @@ Through this, the inverter then establishes a connection to the proxy and the pr By means of `docker` a simple installation and operation is possible. By using `docker-composer`, a complete stack of proxy, `MQTT-brocker` and `home-assistant` can be started easily. -## - +
ℹ️ This project is not related to the company TSUN. It is a private initiative that aims to connect TSUN inverters with an MQTT broker. There is no support and no warranty from TSUN. - -## +

```txt ❗An essential requirement is that the proxy can be looped into the connection From c4d9b10d0ff5c648134412f62baf55a8c075ebb0 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Mon, 15 Apr 2024 22:02:22 +0200 Subject: [PATCH 20/37] initial commit --- .markdownlint.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .markdownlint.json diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..9f6d19c --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "MD013": false, + "MD033": false + } \ No newline at end of file From 8314fd177a937b4083eda85d2675ff547673e95e Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Mon, 15 Apr 2024 23:32:29 +0200 Subject: [PATCH 21/37] improve config description --- README.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cb85d3e..98adf5b 100644 --- a/README.md +++ b/README.md @@ -86,13 +86,28 @@ docker run --dns '8.8.8.8' --env 'UID=1050' -p '5005:5005' -p '10000:10000' -v # Configuration -The Docker container does not require any special configuration. +The configuration consists of several parts. First, the container and the proxy itself must be configured, and then the connection of the inverter to the proxy must be set up, which is done differently depending on the inverter generation + +For GEN3PLUS inverters, this can be done easily via the web interface of the inverter. The GEN3 inverters do not have a web interface, so the proxy is integrated via a modified DNS resolution. + + 1. [Container Setup](#container-setup) + 2. [Proxy Configuration](#proxy-configuration) + 3. [Inverter Configuration](#inverter-configuration) (only GEN3PLUS) + 4. [DNS Settings](#dns-settings) (Mandatory for GEN3) + +## Container Setup + +No special configuration is required for the Docker container if it is built and started as described above. It is recommended to start the container with docker-compose. The configuration is then specified in a docker-compose.yaml file. An example of a stack consisting of the proxy, MQTT broker and home assistant can be found [here](https://github.com/s-allius/tsun-gen3-proxy/blob/main/docker-compose.yaml). + On the host, two directories (for log files and for config files) must be mapped. If necessary, the UID of the proxy process can be adjusted, which is also the owner of the log and configuration files. -The proxy can be configured via the file 'config.toml'. When the proxy is started, a file 'config.example.toml' is copied into the config directory. This file shows all possible parameters and their default values. Changes in the example file itself are not evaluated. To configure the proxy, the config.example.toml file should be renamed to config.toml. After that the corresponding values can be adjusted. To load the new configuration, the proxy must be restarted. +A description of the configuration parameters can be found [here](https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#docker-compose-environment-variables). + ## Proxy Configuration +The proxy can be configured via the file 'config.toml'. When the proxy is started, a file 'config.example.toml' is copied into the config directory. This file shows all possible parameters and their default values. Changes in the example file itself are not evaluated. To configure the proxy, the config.example.toml file should be renamed to config.toml. After that the corresponding values can be adjusted. To load the new configuration, the proxy must be restarted. + The configration uses the TOML format, which aims to be easy to read due to obvious semantics. You find more details here: @@ -154,6 +169,8 @@ pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module de ``` +## Inverter Configuration + ## DNS Settings ### Loop the proxy into the connection @@ -182,6 +199,8 @@ Even if the proxy is connected between the inverter and the TSUN Cloud, an OTA u It must be ensured that this address is not mapped to the proxy! +# General Information + ## Compatibility In the following table you will find an overview of which inverter model has been tested for compatibility with which firmware version. From 2763853b76d9c6e5befd9aefe1ec5e16e7d7aa2c Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 16 Apr 2024 00:07:57 +0200 Subject: [PATCH 22/37] fix linter warnings --- CODE_OF_CONDUCT.md | 8 ++++---- CONTRIBUTING.md | 1 + LICENSE.md | 2 +- README.md | 3 +-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index b651226..2d2effd 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -60,7 +60,7 @@ representative at an online or offline event. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at -compliance@allius.de. +. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the @@ -116,7 +116,7 @@ the community. This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. +. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). @@ -124,5 +124,5 @@ enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. +. Translations are available at +. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5de5b0c..c39ec68 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,7 @@ The project aims to bring TSUN third generation inverters (with WiFi support) in The code base of the proxy was created in a few weeks after work and offers many possibilities for collaboration. Especially in the area of + - docker compose - packaging - test automation diff --git a/LICENSE.md b/LICENSE.md index dc6b3ba..eaeade6 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2023 Stefan Allius. +# Copyright © 2023 Stefan Allius Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/README.md b/README.md index 98adf5b..7a0e4c3 100644 --- a/README.md +++ b/README.md @@ -97,13 +97,12 @@ For GEN3PLUS inverters, this can be done easily via the web interface of the inv ## Container Setup -No special configuration is required for the Docker container if it is built and started as described above. It is recommended to start the container with docker-compose. The configuration is then specified in a docker-compose.yaml file. An example of a stack consisting of the proxy, MQTT broker and home assistant can be found [here](https://github.com/s-allius/tsun-gen3-proxy/blob/main/docker-compose.yaml). +No special configuration is required for the Docker container if it is built and started as described above. It is recommended to start the container with docker-compose. The configuration is then specified in a docker-compose.yaml file. An example of a stack consisting of the proxy, MQTT broker and home assistant can be found [here](https://github.com/s-allius/tsun-gen3-proxy/blob/main/docker-compose.yaml). On the host, two directories (for log files and for config files) must be mapped. If necessary, the UID of the proxy process can be adjusted, which is also the owner of the log and configuration files. A description of the configuration parameters can be found [here](https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details#docker-compose-environment-variables). - ## Proxy Configuration The proxy can be configured via the file 'config.toml'. When the proxy is started, a file 'config.example.toml' is copied into the config directory. This file shows all possible parameters and their default values. Changes in the example file itself are not evaluated. To configure the proxy, the config.example.toml file should be renamed to config.toml. After that the corresponding values can be adjusted. To load the new configuration, the proxy must be restarted. From d85206c12b7beda5d67c8b87eed12554671b0df5 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 16 Apr 2024 19:04:22 +0200 Subject: [PATCH 23/37] add chapter inverter configuration --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7a0e4c3..123ca38 100644 --- a/README.md +++ b/README.md @@ -165,11 +165,20 @@ pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module de pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr - ``` ## Inverter Configuration +GEN3PLUS inverters offer a web interface that can be used to configure the inverter. This is very practical for sending the data directly to the proxy. On the one hand, the inverter broadcasts its own SSID on 2.4GHz. This can be recognized because it is broadcast with `AP_`. You will find the `Monitor SN` and the password for the WLAN connection on a small sticker enclosed with the inverter. + +If you have already connected the inverter to the cloud via the TSUN app, you can also address the inverter directly via WiFi. In the first case, the inverter uses the fixed IP address `10.10.100.254`, in the second case you have to look up the IP address in your router. + +The standard web interface of the inverter can be accessed at `http:///index_cn.html`. Here you can set up the WLAN connection or change the password. The default user and password is `admin`/`admin`. + +For our purpose, the hidden URL `http:///config_hide.html` should be called. There you can see and modify the parameters for accessing the cloud. Here we enter the IP address of our proxy and the IP port 10000 for the `Server A Setting` and for `Optional Server Setting`. The second entry is used as a backup in the event of connection problems. + +If access to the web interface does not work, it can also be redirected via DNS redirection, as is necessary for the GEN3 inverters. + ## DNS Settings ### Loop the proxy into the connection From 3b2028c4c256c0c848b5343584d1bcf2458201f6 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 16 Apr 2024 19:06:59 +0200 Subject: [PATCH 24/37] improve the README.md file --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 123ca38..1a26179 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ If you have already connected the inverter to the cloud via the TSUN app, you ca The standard web interface of the inverter can be accessed at `http:///index_cn.html`. Here you can set up the WLAN connection or change the password. The default user and password is `admin`/`admin`. -For our purpose, the hidden URL `http:///config_hide.html` should be called. There you can see and modify the parameters for accessing the cloud. Here we enter the IP address of our proxy and the IP port 10000 for the `Server A Setting` and for `Optional Server Setting`. The second entry is used as a backup in the event of connection problems. +For our purpose, the hidden URL `http:///config_hide.html` should be called. There you can see and modify the parameters for accessing the cloud. Here we enter the IP address of our proxy and the IP port `10000` for the `Server A Setting` and for `Optional Server Setting`. The second entry is used as a backup in the event of connection problems. If access to the web interface does not work, it can also be redirected via DNS redirection, as is necessary for the GEN3 inverters. From 3a5e4648a1923895646dfd3586d078487b4ed35e Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 16 Apr 2024 19:26:52 +0200 Subject: [PATCH 25/37] Convert the temperature to Grand Celsius --- app/src/gen3plus/infos_g3p.py | 4 ++-- app/tests/test_infos_g3p.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index e3a15dd..1f2a256 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -27,8 +27,8 @@ class RegisterMap: 0x420100d2: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 0x420100d4: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 0x420100d6: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 - # 0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'eval': '(result-32)/1.8'}, # noqa: E501 - 0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H'}, # noqa: E501 + 0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'eval': 'round((result-32)/1.8, 1)'}, # noqa: E501 + # 0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H'}, # noqa: E501 0x420100dc: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501 0x420100de: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 0x420100e0: {'reg': Register.PV1_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index 293c44d..f655afa 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -86,7 +86,7 @@ def test_parse_4210(InverterData: bytes): "controller": {"Power_On_Time": 776}, "inverter": {"Serial_Number": "Y17E00000000000E", "Rated_Power": 600, "Max_Designed_Power": 2000, "No_Inputs": 4}, "grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8}, - "env": {"Inverter_Temp": 54}, + "env": {"Inverter_Temp": 12.2}, "input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76}, "pv2": {"Voltage": 34.6, "Current": 1.38, "Power": 48.4, "Daily_Generation": 0.03, "Total_Generation": 27.91}, "pv3": {"Voltage": 34.6, "Current": 1.89, "Power": 65.5, "Daily_Generation": 0.05, "Total_Generation": 31.89}, From 6035e5223413c89197762fc2fb515ab8f6eec0e7 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Wed, 17 Apr 2024 22:02:21 +0200 Subject: [PATCH 26/37] add Power on Time register for ftype 0x81 --- app/src/gen3plus/infos_g3p.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index 1f2a256..edaab4f 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -56,6 +56,8 @@ class RegisterMap: 0x42010126: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501 0x42010170: {'reg': Register.NO_INPUTS, 'fmt': '!B'}, # noqa: E501 + 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '!H', 'ratio': 1}, # noqa: E501 + } From 82514e9e411e214a884a1d8482cbd05589d82765 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Wed, 17 Apr 2024 22:03:12 +0200 Subject: [PATCH 27/37] calculate real timestamp for received data --- app/src/gen3plus/solarman_v5.py | 47 +++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index fe1e347..554366c 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -55,6 +55,7 @@ class SolarmanV5(Message): self.seq = Sequence(server_side) self.snr = 0 self.db = InfosG3P() + self.time_ofs = 0 self.switch = { 0x4210: self.msg_data_ind, # real time data @@ -352,10 +353,11 @@ class SolarmanV5(Message): total = result[1] tim = result[2] res = result[3] # always zero - logger.info(f'frame type:{ftype:02x} total:{total}s' + logger.info(f'frame type:{ftype:02x}' f' timer:{tim:08x}s null:{res}') - dt = datetime.fromtimestamp(total) - logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}') + if self.time_ofs: + dt = datetime.fromtimestamp(total + self.time_ofs) + logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}') self.__process_data(ftype) self.__forward_msg() @@ -363,27 +365,33 @@ class SolarmanV5(Message): def msg_data_ind(self): data = self._recv_buffer - result = struct.unpack_from(' Date: Wed, 17 Apr 2024 22:05:24 +0200 Subject: [PATCH 28/37] adapt container informations --- app/Dockerfile | 3 ++- app/build.sh | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/Dockerfile b/app/Dockerfile index 6b796c1..30cdae8 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -71,8 +71,9 @@ ENTRYPOINT ["/root/entrypoint.sh"] CMD [ "python3", "./server.py" ] +LABEL org.opencontainers.image.title="TSUN Gen3 Proxy" LABEL org.opencontainers.image.authors="Stefan Allius" LABEL org.opencontainers.image.source https://github.com/s-allius/tsun-gen3-proxy -LABEL org.opencontainers.image.description 'The "TSUN Gen3 Micro-Inverter" proxy enables a reliable connection between TSUN third generation inverters and an MQTT broker to integrate the inverter into typical home automations' +LABEL org.opencontainers.image.description 'This proxy enables a reliable connection between TSUN third generation inverters (eg. TSOL MS600, MS800, MS2000) and an MQTT broker to integrate the inverter into typical home automations.' LABEL org.opencontainers.image.licenses="BSD-3-Clause" LABEL org.opencontainers.image.vendor="Stefan Allius" diff --git a/app/build.sh b/app/build.sh index bb89cfa..ac8879d 100755 --- a/app/build.sh +++ b/app/build.sh @@ -20,7 +20,7 @@ IMAGE=tsun-gen3-proxy if [[ $1 == dev ]] || [[ $1 == rc ]] ;then IMAGE=docker.io/sallius/${IMAGE} -VERSION=${VERSION}-$1-${BRANCH} +VERSION=${VERSION} elif [[ $1 == rel ]];then IMAGE=ghcr.io/s-allius/${IMAGE} else @@ -31,11 +31,11 @@ fi echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE if [[ $1 == dev ]];then -docker build --build-arg "VERSION=${VERSION}" --build-arg environment=dev --build-arg "LOG_LVL=DEBUG" --label "org.label-schema.build-date=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" -t ${IMAGE}:latest app +docker build --build-arg "VERSION=${VERSION}" --build-arg environment=dev --build-arg "LOG_LVL=DEBUG" --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:latest app elif [[ $1 == rc ]];then -docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.label-schema.build-date=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" -t ${IMAGE}:latest app +docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:latest app elif [[ $1 == rel ]];then -docker build --no-cache --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.label-schema.build-date=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" -t ${IMAGE}:latest -t ${IMAGE}:${MAJOR} -t ${IMAGE}:${VERSION} app +docker build --no-cache --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:latest -t ${IMAGE}:${MAJOR} -t ${IMAGE}:${VERSION} app echo 'login to ghcr.io' echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin docker push ghcr.io/s-allius/tsun-gen3-proxy:latest From ee1722e374a3c4daf8de0d1ed1f667a923fdd5a7 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Thu, 18 Apr 2024 18:44:09 +0200 Subject: [PATCH 29/37] decode logger values as little endian --- app/src/gen3plus/infos_g3p.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index edaab4f..1849979 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -14,15 +14,15 @@ class RegisterMap: __slots__ = () map = { # 0x41020007: {'reg': Register.DEVICE_SNR, 'fmt': ' Date: Thu, 18 Apr 2024 18:45:01 +0200 Subject: [PATCH 30/37] fix incomplete format string --- app/src/gen3plus/solarman_v5.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index 554366c..7b64be3 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -415,7 +415,7 @@ class SolarmanV5(Message): def msg_sync_end(self): data = self._recv_buffer[self.header_len:] - result = struct.unpack_from(' Date: Thu, 18 Apr 2024 19:06:40 +0200 Subject: [PATCH 31/37] fix endianess of Power_on_time test --- app/tests/test_infos_g3p.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index f655afa..ace0542 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -83,7 +83,7 @@ def test_parse_4210(InverterData: bytes): pass assert json.dumps(i.db) == json.dumps({ - "controller": {"Power_On_Time": 776}, + "controller": {"Power_On_Time": 2051}, "inverter": {"Serial_Number": "Y17E00000000000E", "Rated_Power": 600, "Max_Designed_Power": 2000, "No_Inputs": 4}, "grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8}, "env": {"Inverter_Temp": 12.2}, From 6f9d2d4facf917c1614179f02424c33b419723bf Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 19 Apr 2024 19:07:59 +0200 Subject: [PATCH 32/37] GEN3PLUS: Add inverter status --- app/src/gen3plus/infos_g3p.py | 1 + app/src/infos.py | 3 +++ app/tests/test_infos_g3p.py | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index 1849979..282e136 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -24,6 +24,7 @@ class RegisterMap: 0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': ' Date: Fri, 19 Apr 2024 21:29:14 +0200 Subject: [PATCH 33/37] read inverter & logger version --- app/src/gen3plus/infos_g3p.py | 5 +++-- app/src/gen3plus/solarman_v5.py | 10 ---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index 282e136..25bb91a 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -18,13 +18,14 @@ class RegisterMap: 0x41020019: {'reg': Register.COLLECT_INTERVAL, 'fmt': '>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501 0x420100d2: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 0x420100d4: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 0x420100d6: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index 7b64be3..d23efa3 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -305,14 +305,11 @@ class SolarmanV5(Message): def __process_data(self, ftype): inv_update = False - ctrl_update = False msg_type = self.control >> 8 for key, update in self.db.parse(self._recv_buffer, msg_type, ftype): if update: if key == 'inverter': inv_update = True - if key == 'controller': - ctrl_update = True self.new_data[key] = True if inv_update: @@ -331,13 +328,6 @@ class SolarmanV5(Message): logger.info(f'Model: {Model}') self.db.set_db_def_value(Register.EQUIPMENT_MODEL, Model) - if ctrl_update: - db = self.db - Version = db.get_db_value(Register.COLLECTOR_FW_VERSION, 0) - if isinstance(Version, str): - Model = Version.split('_')[0] - self.db.set_db_def_value(Register.CHIP_MODEL, Model) - ''' Message handler methods ''' From a571a3b4564d61ca62352123ac29ae77b7246524 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 19 Apr 2024 21:30:41 +0200 Subject: [PATCH 34/37] adapt testcases to new version reading --- app/tests/test_infos_g3p.py | 5 ++--- app/tests/test_solarman.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index bcebe10..850dc67 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -71,8 +71,7 @@ def test_parse_4110(DeviceData: bytes): assert json.dumps(i.db) == json.dumps({ 'controller': {"Data_Up_Interval": 300, "Collect_Interval": 60, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Adress": "192.168.80.49"}, - 'collector': {"Collector_Fw_Version": "LSW5BLE_17_02B0_1.05"}, - 'inverter': {"Version": "V1.1.00.0B"}, + 'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "Collector_Fw_Version": "V1.1.00.0B"}, }) def test_parse_4210(InverterData: bytes): @@ -84,7 +83,7 @@ def test_parse_4210(InverterData: bytes): assert json.dumps(i.db) == json.dumps({ "controller": {"Power_On_Time": 2051}, - "inverter": {"Serial_Number": "Y17E00000000000E", "Rated_Power": 600, "Max_Designed_Power": 2000, "No_Inputs": 4}, + "inverter": {"Serial_Number": "Y17E00000000000E", "Version": "v4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000, "No_Inputs": 4}, "env": {"Inverter_Status": 1, "Inverter_Temp": 12.2}, "grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8}, "input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76}, diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py index 0f54f0c..48f5509 100644 --- a/app/tests/test_solarman.py +++ b/app/tests/test_solarman.py @@ -938,8 +938,8 @@ def test_build_logger_modell(ConfigTsunAllowAll, DeviceIndMsg): assert 'IGEN TECH' == m.db.get_db_value(Register.CHIP_TYPE, None) assert None == m.db.get_db_value(Register.CHIP_MODEL, None) m.read() # read complete msg, and dispatch msg - assert 'LSW5BLE_17_02B0_1.05' == m.db.get_db_value(Register.COLLECTOR_FW_VERSION, 0).rstrip('\00') - assert 'LSW5BLE' == m.db.get_db_value(Register.CHIP_MODEL, 0) + assert 'LSW5BLE_17_02B0_1.05' == m.db.get_db_value(Register.CHIP_MODEL, 0) + assert 'V1.1.00.0B' == m.db.get_db_value(Register.COLLECTOR_FW_VERSION, 0).rstrip('\00') m.close() def test_AT_cmd(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, AtCommandIndMsg): From 4faf44db91c3c25ba76c5e53552135d8f84e1545 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 20 Apr 2024 00:05:34 +0200 Subject: [PATCH 35/37] GEN3PLUS: fix temperature values --- app/src/gen3plus/infos_g3p.py | 2 +- app/tests/test_infos_g3p.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index 25bb91a..a9843cf 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -29,7 +29,7 @@ class RegisterMap: 0x420100d2: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 0x420100d4: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 0x420100d6: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 - 0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'eval': 'round((result-32)/1.8, 1)'}, # noqa: E501 + 0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'eval': 'result-40'}, # noqa: E501 # 0x420100d8: {'reg': Register.INVERTER_TEMP, 'fmt': '!H'}, # noqa: E501 0x420100dc: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501 0x420100de: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index 850dc67..c5609be 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -84,7 +84,7 @@ def test_parse_4210(InverterData: bytes): assert json.dumps(i.db) == json.dumps({ "controller": {"Power_On_Time": 2051}, "inverter": {"Serial_Number": "Y17E00000000000E", "Version": "v4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000, "No_Inputs": 4}, - "env": {"Inverter_Status": 1, "Inverter_Temp": 12.2}, + "env": {"Inverter_Status": 1, "Inverter_Temp": 14}, "grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8}, "input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76}, "pv2": {"Voltage": 34.6, "Current": 1.38, "Power": 48.4, "Daily_Generation": 0.03, "Total_Generation": 27.91}, From 5130211985c2b246569d176d4b8d0a2619b9b7bd Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 20 Apr 2024 01:19:26 +0200 Subject: [PATCH 36/37] Update changelog --- CHANGELOG.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70a27d4..4c07c82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- GEN3PLUS: fix temperature values +- GEN3PLUS: read corect firmware and logger version +- GEN3PLUS: add inverter status +- GEN3PLUS: fix encoding of `power on time` value +- GEN3PLUS: fix glitches in inverter data after connection establishment + see: [#53](https://github.com/s-allius/tsun-gen3-proxy/issues/53) +- improve docker container labels +- GEN3PLUS: add timestamp of inverter data into log +- config linter for *.md files - switch to aiomqtt version 2.0.1 - refactor unittest and increase testcoverage -- add experimental handler for `ÀT` commands -- implement self-sufficient island support for GEN3PLUS inverters +- GEN3PLUS: add experimental handler for `ÀT` commands +- GEN3PLUS: implement self-sufficient island support see: [#42](https://github.com/s-allius/tsun-gen3-proxy/issues/42) - Improve error messages on config errors see: [#46](https://github.com/s-allius/tsun-gen3-proxy/issues/46) From f29de66477246db0cf58ebfbf7079a6eb33cbce1 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 20 Apr 2024 01:54:09 +0200 Subject: [PATCH 37/37] fix warning in CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c07c82..cf76666 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 see: [#53](https://github.com/s-allius/tsun-gen3-proxy/issues/53) - improve docker container labels - GEN3PLUS: add timestamp of inverter data into log -- config linter for *.md files +- config linter for *.md files - switch to aiomqtt version 2.0.1 - refactor unittest and increase testcoverage - GEN3PLUS: add experimental handler for `ÀT` commands