From 5d0c95d6e66f9a2779eaffaa9eec1ca5387d392f Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Wed, 1 May 2024 11:57:02 +0200 Subject: [PATCH 01/29] fix typo --- app/src/gen3plus/infos_g3p.py | 2 +- app/src/infos.py | 6 ++++-- app/tests/test_infos_g3p.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index 0191d04..b0adc0a 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -19,7 +19,7 @@ class RegisterMap: 0x4102001a: {'reg': Register.HEARTBEAT_INTERVAL, 'fmt': ' Date: Wed, 1 May 2024 11:57:32 +0200 Subject: [PATCH 02/29] Add Modbus_Command counter --- app/tests/test_infos.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/tests/test_infos.py b/app/tests/test_infos.py index d3b542e..c3e6ddf 100644 --- a/app/tests/test_infos.py +++ b/app/tests/test_infos.py @@ -17,13 +17,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, "Invalid_Msg_Format": 0, "AT_Command": 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, "AT_Command": 0, "Modbus_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}}) + 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, "Modbus_Command": 0}}) val = i.dev_value(Register.INVERTER_CNT) assert val == 1 From 58c3333fcc85a1686594306c97e697998015d339 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Thu, 2 May 2024 23:55:59 +0200 Subject: [PATCH 03/29] initial checkin --- app/src/modbus.py | 56 ++++++++++++++++++++++++++++++++++++++++ app/src/singleton.py | 9 +++++++ app/tests/test_modbus.py | 21 +++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 app/src/modbus.py create mode 100644 app/src/singleton.py create mode 100644 app/tests/test_modbus.py diff --git a/app/src/modbus.py b/app/src/modbus.py new file mode 100644 index 0000000..9745f23 --- /dev/null +++ b/app/src/modbus.py @@ -0,0 +1,56 @@ +import struct + +if __name__ == "app.src.modbus": + from app.src.singleton import Singleton +else: # pragma: no cover + from singleton import Singleton + +####### +# TSUN uses the Modbus in the RTU transmission mode. +# see: https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf +# +# A Modbus PDU consists of: 'Function-Code' + 'Data' +# A Modbus RTU message consists of: 'Addr' + 'Modbus-PDU' + 'CRC-16' +# +# The 16-bit CRC is known as CRC-16-ANSI(reverse) +# see: https://en.wikipedia.org/wiki/Computation_of_cyclic_redundancy_checks +####### + +CRC_POLY = 0xA001 # (LSBF/reverse) +CRC_INIT = 0xFFFF + + +class Modbus(metaclass=Singleton): + MB_WRITE_SINGLE_REG = 6 + MB_READ_SINGLE_REG = 3 + __crc_tab = [] + + def __init__(self): + self.__build_crc_tab(CRC_POLY) + + def build_msg(self, addr, func, reg, val): + msg = struct.pack('>BBHH', addr, func, reg, val) + msg += struct.pack(' bool: + return 0 == self.__calc_crc(msg) + + def __calc_crc(self, buffer: bytes) -> int: + crc = CRC_INIT + + for cur in buffer: + crc = (crc >> 8) ^ self.__crc_tab[(crc ^ cur) & 0xFF] + return crc + + def __build_crc_tab(self, poly) -> None: + for index in range(256): + data = index << 1 + crc = 0 + for _ in range(8, 0, -1): + data >>= 1 + if (data ^ crc) & 1: + crc = (crc >> 1) ^ poly + else: + crc >>= 1 + self.__crc_tab.append(crc) diff --git a/app/src/singleton.py b/app/src/singleton.py new file mode 100644 index 0000000..48778b9 --- /dev/null +++ b/app/src/singleton.py @@ -0,0 +1,9 @@ +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + # logger_mqtt.debug('singleton: __call__') + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, + cls).__call__(*args, **kwargs) + return cls._instances[cls] diff --git a/app/tests/test_modbus.py b/app/tests/test_modbus.py new file mode 100644 index 0000000..0e9cf5b --- /dev/null +++ b/app/tests/test_modbus.py @@ -0,0 +1,21 @@ +# test_with_pytest.py +# import pytest, logging +from app.src.modbus import Modbus + + +def test_modbus_crc(): + mb = Modbus() + assert 0x0b02 == mb._Modbus__calc_crc(b'\x01\x06\x20\x08\x00\x04') + assert 0 == mb._Modbus__calc_crc(b'\x01\x06\x20\x08\x00\x04\x02\x0b') + assert mb.check_crc(b'\x01\x06\x20\x08\x00\x04\x02\x0b') + + assert 0xc803 == mb._Modbus__calc_crc(b'\x01\x06\x20\x08\x00\x00') + assert 0 == mb._Modbus__calc_crc(b'\x01\x06\x20\x08\x00\x00\x03\xc8') + assert mb.check_crc(b'\x01\x06\x20\x08\x00\x00\x03\xc8') + +def test_build_modbus_pdu(): + mb = Modbus() + pdu = mb.build_msg(1,6,0x2000,0x12) + assert pdu == b'\x01\x06\x20\x00\x00\x12\x02\x07' + assert mb.check_crc(pdu) + From 1d9cbf314e96d0bd61d383ee3eba8fd03c11e37e Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Thu, 2 May 2024 23:56:42 +0200 Subject: [PATCH 04/29] add Modbus tests --- app/tests/test_talent.py | 122 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/app/tests/test_talent.py b/app/tests/test_talent.py index 89fd420..fc4ed4e 100644 --- a/app/tests/test_talent.py +++ b/app/tests/test_talent.py @@ -3,6 +3,10 @@ import pytest, logging from app.src.gen3.talent import Talent, Control from app.src.config import Config from app.src.infos import Infos +from app.src.modbus import Modbus + + +pytest_plugins = ('pytest_asyncio',) # initialize the proxy statistics Infos.static_init() @@ -19,6 +23,7 @@ class MemoryStream(Talent): self.__chunk_idx = 0 self.msg_count = 0 self.addr = 'Test: SrvSide' + self.send_msg_ofs = 0 def append_msg(self, msg): self.__msg += msg @@ -50,6 +55,10 @@ class MemoryStream(Talent): self.msg_count += 1 return + async def flush_send_msg(self): + pass + + @pytest.fixture def MsgContactInfo(): # Contact Info message @@ -170,6 +179,26 @@ def MsgOtaAck(): # Over the air update rewuest from tsun cloud def MsgOtaInvalid(): # Get Time Request message return b'\x00\x00\x00\x14\x10R170000000000001\x99\x13\x01' +@pytest.fixture +def MsgModbusCmd(): + msg = b'\x00\x00\x00\x20\x10R170000000000001' + msg += b'\x70\x77\x00\x01\xa3\x28\x08\x01\x06\x20\x08' + msg += b'\x00\x00\x03\xc8' + return msg + +@pytest.fixture +def MsgModbusRsp(): + msg = b'\x00\x00\x00\x20\x10R170000000000001' + msg += b'\x91\x77\x17\x18\x19\x1a\x08\x01\x06\x20\x08' + msg += b'\x00\x00\x03\xc8' + return msg + +@pytest.fixture +def MsgModbusInv(): + msg = b'\x00\x00\x00\x20\x10R170000000000001' + msg += b'\x99\x77\x17\x18\x19\x1a\x08\x01\x06\x20\x08' + msg += b'\x00\x00\x03\xc8' + return msg def test_read_message(MsgContactInfo): m = MemoryStream(MsgContactInfo, (0,)) @@ -740,3 +769,96 @@ def test_proxy_counter(): assert Infos.new_stat_data == {'proxy': True} assert 0 == m.db.stat['proxy']['Unknown_Msg'] m.close() + +def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd): + ConfigTsunInv1 + m = MemoryStream(MsgModbusCmd, (0,), False) + m.db.stat['proxy']['Unknown_Ctrl'] = 0 + m.db.stat['proxy']['Modbus_Command'] = 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.unique_id == 'R170000000000001' + assert int(m.ctrl)==112 + assert m.msg_id==119 + assert m.header_len==23 + assert m.data_len==13 + assert m._forward_buffer==MsgModbusCmd + assert m._send_buffer==b'' + assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 + assert m.db.stat['proxy']['Modbus_Command'] == 1 + m.close() + +def test_msg_modbus_rsp1(ConfigTsunInv1, MsgModbusRsp): + ConfigTsunInv1 + m = MemoryStream(MsgModbusRsp, (0,), False) + m.db.stat['proxy']['Unknown_Ctrl'] = 0 + m.db.stat['proxy']['Modbus_Command'] = 0 + m.forward_modbus_rep = 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.id_str == b"R170000000000001" + assert m.unique_id == 'R170000000000001' + assert int(m.ctrl)==145 + assert m.msg_id==119 + assert m.header_len==23 + assert m.data_len==13 + assert m._forward_buffer==b'' + assert m._send_buffer==b'' + assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 + assert m.db.stat['proxy']['Modbus_Command'] == 0 + m.close() + +def test_msg_modbus_rsp2(ConfigTsunInv1, MsgModbusRsp): + ConfigTsunInv1 + m = MemoryStream(MsgModbusRsp, (0,), False) + m.db.stat['proxy']['Unknown_Ctrl'] = 0 + m.db.stat['proxy']['Modbus_Command'] = 0 + m.forward_modbus_rep = True + 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.unique_id == 'R170000000000001' + assert int(m.ctrl)==145 + assert m.msg_id==119 + assert m.header_len==23 + assert m.data_len==13 + assert m._forward_buffer==MsgModbusRsp + assert m._send_buffer==b'' + assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 + assert m.db.stat['proxy']['Modbus_Command'] == 0 + m.close() + +def test_msg_modbus_invalid(ConfigTsunInv1, MsgModbusInv): + ConfigTsunInv1 + m = MemoryStream(MsgModbusInv, (0,), False) + m.db.stat['proxy']['Unknown_Ctrl'] = 0 + m.db.stat['proxy']['Modbus_Command'] = 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.unique_id == 'R170000000000001' + assert int(m.ctrl)==153 + assert m.msg_id==119 + assert m.header_len==23 + assert m.data_len==13 + assert m._forward_buffer==MsgModbusInv + assert m._send_buffer==b'' + assert m.db.stat['proxy']['Unknown_Ctrl'] == 1 + assert m.db.stat['proxy']['Modbus_Command'] == 0 + m.close() + +@pytest.mark.asyncio +async def test_msg_build_modbus_req(ConfigTsunInv1, MsgModbusCmd): + ConfigTsunInv1 + m = MemoryStream(b'', (0,), False) + m.id_str = b"R170000000000001" + await m.send_modbus_cmd(Modbus.MB_WRITE_SINGLE_REG, 0x2008, 0) + assert 0==m.send_msg_ofs + assert m._forward_buffer==b'' + assert m._send_buffer==MsgModbusCmd + m.close() From dba3b458ba34d787c5afae755da22ea9d542c7d5 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Thu, 2 May 2024 23:59:55 +0200 Subject: [PATCH 05/29] add Modbus support --- app/src/gen3/talent.py | 58 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index 46302ac..f983c79 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -3,12 +3,15 @@ import logging import time from datetime import datetime + if __name__ == "app.src.gen3.talent": from app.src.messages import hex_dump_memory, Message + from app.src.modbus import Modbus from app.src.config import Config from app.src.gen3.infos_g3 import InfosG3 else: # pragma: no cover from messages import hex_dump_memory, Message + from modbus import Modbus from config import Config from gen3.infos_g3 import InfosG3 @@ -41,13 +44,18 @@ class Talent(Message): self.contact_name = b'' self.contact_mail = b'' self.db = InfosG3() + self.forward_modbus_rep = False self.switch = { 0x00: self.msg_contact_info, 0x13: self.msg_ota_update, 0x22: self.msg_get_time, 0x71: self.msg_collector_data, + # 0x76: + 0x77: self.msg_modbus, + # 0x78: 0x04: self.msg_inverter_data, } + self.mb = Modbus() ''' Our puplic methods @@ -115,6 +123,18 @@ class Talent(Message): f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}') return + async def send_modbus_cmd(self, func, addr, val) -> None: + self.forward_modbus_rep = False + self.__build_header(0x70, 0x77) + self._send_buffer += b'\x00\x01\xa3\x28' + modbus_msg = self.mb.build_msg(1, func, addr, val) + self._send_buffer += struct.pack('!B', len(modbus_msg)) + self._send_buffer += modbus_msg + _len = self.__finish_send_msg() + hex_dump_memory(logging.INFO, 'Send Modbus Command:', + self._send_buffer[self.send_msg_ofs:], _len) + await self.flush_send_msg() + def _init_new_client_conn(self) -> bool: contact_name = self.contact_name contact_mail = self.contact_mail @@ -190,17 +210,20 @@ class Talent(Message): self.header_valid = True return - def __build_header(self, ctrl) -> None: + def __build_header(self, ctrl, msg_id=None) -> None: + if not msg_id: + msg_id = self.msg_id self.send_msg_ofs = len(self._send_buffer) self._send_buffer += struct.pack(f'!l{len(self.id_str)+1}pBB', - 0, self.id_str, ctrl, self.msg_id) - fnc = self.switch.get(self.msg_id, self.msg_unknown) + 0, self.id_str, ctrl, msg_id) + fnc = self.switch.get(msg_id, self.msg_unknown) logger.info(self.__flow_str(self.server_side, 'tx') + f' Ctl: {int(ctrl):#02x} Msg: {fnc.__name__!r}') - def __finish_send_msg(self) -> None: + def __finish_send_msg(self) -> int: _len = len(self._send_buffer) - self.send_msg_ofs struct.pack_into('!l', self._send_buffer, self.send_msg_ofs, _len-4) + return _len def __dispatch_msg(self) -> None: fnc = self.switch.get(self.msg_id, self.msg_unknown) @@ -348,6 +371,33 @@ class Talent(Message): self.inc_counter('Unknown_Ctrl') self.forward(self._recv_buffer, self.header_len+self.data_len) + def parse_modbus_header(self): + + msg_hdr_len = 5 + + result = struct.unpack_from('!lB', self._recv_buffer, + self.header_len + 4) + modbus_len = result[1] + logger.debug(f'Ref: {result[0]}') + logger.debug(f'Modbus Len: {modbus_len}') + # logger.info(f'time: {datetime.utcfromtimestamp(result[2]).strftime( + # "%Y-%m-%d %H:%M:%S")}') + return msg_hdr_len, modbus_len + + def msg_modbus(self): + hdr_len, modbus_len = self.parse_modbus_header() + + if self.ctrl.is_req(): + self.forward_modbus_rep = True + self.inc_counter('Modbus_Command') + elif self.ctrl.is_ind(): + if not self.forward_modbus_rep: + return + else: + logger.warning('Unknown Ctrl') + self.inc_counter('Unknown_Ctrl') + self.forward(self._recv_buffer, self.header_len+self.data_len) + def msg_unknown(self): logger.warning(f"Unknow Msg: ID:{self.msg_id}") self.inc_counter('Unknown_Msg') From 5fdad484f480860bc96899b54fb078454eee5a83 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 3 May 2024 00:03:02 +0200 Subject: [PATCH 06/29] add flush_send_msg() implementation --- app/src/gen3/connection_g3.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/gen3/connection_g3.py b/app/src/gen3/connection_g3.py index c93156e..3ac1e7c 100644 --- a/app/src/gen3/connection_g3.py +++ b/app/src/gen3/connection_g3.py @@ -29,6 +29,11 @@ class ConnectionG3(AsyncStream, Talent): async def async_publ_mqtt(self) -> None: pass + async def flush_send_msg(self) -> None: + self.writer.write(self._send_buffer) + await self.writer.drain() + self._send_buffer = bytearray(0) + ''' Our private methods ''' From 30dc802fb23cdae858cf362c8df26cd9f0c14adc Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 3 May 2024 00:05:34 +0200 Subject: [PATCH 07/29] Add MQTT subscrition for modbus experiences --- app/src/mqtt.py | 57 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/app/src/mqtt.py b/app/src/mqtt.py index 5b2de02..6a69c95 100644 --- a/app/src/mqtt.py +++ b/app/src/mqtt.py @@ -1,22 +1,15 @@ import asyncio import logging import aiomqtt +import traceback +from modbus import Modbus +from messages import Message from config import Config +from singleton import Singleton logger_mqtt = logging.getLogger('mqtt') -class Singleton(type): - _instances = {} - - def __call__(cls, *args, **kwargs): - logger_mqtt.debug('singleton: __call__') - if cls not in cls._instances: - cls._instances[cls] = super(Singleton, - cls).__call__(*args, **kwargs) - return cls._instances[cls] - - class Mqtt(metaclass=Singleton): __client = None __cb_MqttIsUp = None @@ -65,6 +58,9 @@ class Mqtt(metaclass=Singleton): password=mqtt['passwd']) interval = 5 # Seconds + ha_status_topic = f"{ha['auto_conf_prefix']}/status" + inv_cnf_topic = "tsun/+/test" + while True: try: async with self.__client: @@ -74,16 +70,32 @@ class Mqtt(metaclass=Singleton): await self.__cb_MqttIsUp() # async with self.__client.messages() as messages: - await self.__client.subscribe( - f"{ha['auto_conf_prefix']}" - "/status") + await self.__client.subscribe(ha_status_topic) + await self.__client.subscribe(inv_cnf_topic) + async for message in self.__client.messages: - status = message.payload.decode("UTF-8") - logger_mqtt.info('Home-Assistant Status:' - f' {status}') - if status == 'online': - self.ha_restarts += 1 - await self.__cb_MqttIsUp() + if message.topic.matches(ha_status_topic): + status = message.payload.decode("UTF-8") + logger_mqtt.info('Home-Assistant Status:' + f' {status}') + if status == 'online': + self.ha_restarts += 1 + await self.__cb_MqttIsUp() + + if message.topic.matches(inv_cnf_topic): + topic = str(message.topic) + node_id = topic.split('/')[1] + '/' + payload = message.payload.decode("UTF-8") + logger_mqtt.info(f'InvCnf: {node_id}:{payload}') + for m in Message: + if m.server_side and m.node_id == node_id: + logger_mqtt.info(f'Found: {node_id}') + fnc = getattr(m, "send_modbus_cmd", None) + if callable(fnc): + # await fnc(Modbus.MB_WRITE_SINGLE_REG, + # 0x2008, 2) + await fnc(Modbus.MB_READ_SINGLE_REG, + 0x2008, 1) except aiomqtt.MqttError: if Config.is_default('mqtt'): @@ -101,3 +113,8 @@ class Mqtt(metaclass=Singleton): logger_mqtt.debug("MQTT task cancelled") self.__client = None return + except Exception: + # self.inc_counter('SW_Exception') + logger_mqtt.error( + f"Exception:\n" + f"{traceback.format_exc()}") From 494c30e4898b67ffb602a0ca5df947e4159fef16 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 3 May 2024 18:21:15 +0200 Subject: [PATCH 08/29] renme __async_write() into async_write() --- app/proxy.svg | 562 ++++++++++++++++++++-------------------- app/proxy.yuml | 4 +- app/src/async_stream.py | 8 +- 3 files changed, 293 insertions(+), 281 deletions(-) diff --git a/app/proxy.svg b/app/proxy.svg index 588835e..dfbdd46 100644 --- a/app/proxy.svg +++ b/app/proxy.svg @@ -4,340 +4,352 @@ - + G - + A0 - - - -You can stick notes -on diagrams too! + + + +You can stick notes +on diagrams too! A1 - -Singleton + +Singleton A2 - -Mqtt - -<static>ha_restarts -<static>__client -<static>__cb_MqttIsUp - -<async>publish() -<async>close() + +Mqtt + +<static>ha_restarts +<static>__client +<static>__cb_MqttIsUp + +<async>publish() +<async>close() A1->A2 - - - - - -A10 - -Inverter - -cls.db_stat -cls.entity_prfx -cls.discovery_prfx -cls.proxy_node_id -cls.proxy_unique_id -cls.mqtt:Mqtt - - - - -A2->A10 - + + A3 - -IterRegistry - - -__iter__ + +Modbus - - -A4 - -Message - -server_side:bool -header_valid:bool -header_len:unsigned -data_len:unsigned -unique_id -node_id -sug_area -_recv_buffer:bytearray -_send_buffer:bytearray -_forward_buffer:bytearray -db:Infos -new_data:list - -_read():void<abstract> -close():void -inc_counter():void -dec_counter():void - - + -A3->A4 - - - - - -A5 - -Talent - -await_conn_resp_cnt -id_str -contact_name -contact_mail -switch - -msg_contact_info() -msg_ota_update() -msg_get_time() -msg_collector_data() -msg_inverter_data() -msg_unknown() -close() - - - -A4->A5 - - - - - -A6 - -SolarmanV5 - -control -serial -snr -switch - -msg_unknown() -close() - - - -A4->A6 - - - - - -A7 - -ConnectionG3 - -remoteStream:ConnectionG3 - -close() - - - -A5->A7 - - - - - -A8 - -ConnectionG3P - -remoteStream:ConnectionG3P - -close() - - - -A6->A8 - - - - - -A7->A7 - - -0..1 -has +A1->A3 + + A11 - -InverterG3 - -__ha_restarts - -async_create_remote() -close() + +Inverter + +cls.db_stat +cls.entity_prfx +cls.discovery_prfx +cls.proxy_node_id +cls.proxy_unique_id +cls.mqtt:Mqtt + - + -A7->A11 - - +A2->A11 + - - -A8->A8 - - -0..1 -has + + +A4 + +IterRegistry + + +__iter__ - - -A12 - -InverterG3P - -__ha_restarts - -async_create_remote() -close() + + +A5 + +Message + +server_side:bool +header_valid:bool +header_len:unsigned +data_len:unsigned +unique_id +node_id +sug_area +_recv_buffer:bytearray +_send_buffer:bytearray +_forward_buffer:bytearray +db:Infos +new_data:list + +_read():void<abstract> +close():void +inc_counter():void +dec_counter():void - - -A8->A12 - - + + +A4->A5 + + + + + +A6 + +Talent + +await_conn_resp_cnt +id_str +contact_name +contact_mail +switch + +msg_contact_info() +msg_ota_update() +msg_get_time() +msg_collector_data() +msg_inverter_data() +msg_unknown() +close() + + + +A5->A6 + + + + + +A7 + +SolarmanV5 + +control +serial +snr +switch + +msg_unknown() +close() + + + +A5->A7 + + + + + +A8 + +ConnectionG3 + +remoteStream:ConnectionG3 + +close() + + + +A6->A8 + + A9 - -AsyncStream - -reader -writer -addr -r_addr -l_addr - -<async>server_loop() -<async>client_loop() -<async>loop -disc() -close() -__async_read() -__async_write() -__async_forward() + +ConnectionG3P + +remoteStream:ConnectionG3P + +close() - + -A9->A7 - - +A7->A9 + + - - -A9->A8 - - + + +A8->A8 + + +0..1 +has - - -A10->A11 - - + + +A12 + +InverterG3 + +__ha_restarts + +async_create_remote() +close() - - -A10->A12 - - + + +A8->A12 + + + + + +A9->A9 + + +0..1 +has A13 - -Infos - -stat -new_stat_data -info_dev - -static_init() -dev_value() -inc_counter() -dec_counter() -ha_proxy_conf -ha_conf -update_db -set_db_def_value -get_db_value -ignore_this_device + +InverterG3P + +__ha_restarts + +async_create_remote() +close() + + + +A9->A13 + + + + + +A10 + +AsyncStream + +reader +writer +addr +r_addr +l_addr + +<async>server_loop() +<async>client_loop() +<async>loop +disc() +close() +__async_read() +async_write() +__async_forward() + + + +A10->A8 + + + + + +A10->A9 + + + + + +A11->A12 + + + + + +A11->A13 + + A14 - -InfosG3 - - -ha_confs() -parse() - - - -A13->A14 - - + +Infos + +stat +new_stat_data +info_dev + +static_init() +dev_value() +inc_counter() +dec_counter() +ha_proxy_conf +ha_conf +update_db +set_db_def_value +get_db_value +ignore_this_device A15 - -InfosG3P - - -ha_confs() -parse() + +InfosG3 + + +ha_confs() +parse() - + -A13->A15 - - +A14->A15 + + - - -A14->A5 - - + + +A16 + +InfosG3P + + +ha_confs() +parse() + + + +A14->A16 + + - + A15->A6 - - + + + + + +A16->A7 + + diff --git a/app/proxy.yuml b/app/proxy.yuml index 7f5be21..daf5d3c 100644 --- a/app/proxy.yuml +++ b/app/proxy.yuml @@ -4,13 +4,13 @@ [note: You can stick notes on diagrams too!{bg:cornsilk}] [Singleton]^[Mqtt|ha_restarts;__client;__cb_MqttIsUp|publish();close()] - +[Singleton]^[Modbus] [IterRegistry||__iter__]^[Message|server_side:bool;header_valid:bool;header_len:unsigned;data_len:unsigned;unique_id;node_id;sug_area;_recv_buffer:bytearray;_send_buffer:bytearray;_forward_buffer:bytearray;db:Infos;new_data:list|_read():void;close():void;inc_counter():void;dec_counter():void] [Message]^[Talent|await_conn_resp_cnt;id_str;contact_name;contact_mail;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;close()] [Message]^[SolarmanV5|control;serial;snr;switch|msg_unknown();;close()] [Talent]^[ConnectionG3|remoteStream:ConnectionG3|close()] [SolarmanV5]^[ConnectionG3P|remoteStream:ConnectionG3P|close()] -[AsyncStream|reader;writer;addr;r_addr;l_addr|server_loop();client_loop();loop;disc();close();;__async_read();__async_write();__async_forward()]^[ConnectionG3] +[AsyncStream|reader;writer;addr;r_addr;l_addr|server_loop();client_loop();loop;disc();close();;__async_read();async_write();__async_forward()]^[ConnectionG3] [AsyncStream]^[ConnectionG3P] [Inverter|cls.db_stat;cls.entity_prfx;cls.discovery_prfx;cls.proxy_node_id;cls.proxy_unique_id;cls.mqtt:Mqtt|]^[InverterG3|__ha_restarts|async_create_remote();;close()] [Inverter]^[InverterG3P|__ha_restarts|async_create_remote();;close()] diff --git a/app/src/async_stream.py b/app/src/async_stream.py index 6c1136c..28873e8 100644 --- a/app/src/async_stream.py +++ b/app/src/async_stream.py @@ -61,7 +61,7 @@ class AsyncStream(): await self.__async_read() if self.unique_id: - await self.__async_write() + await self.async_write() await self.__async_forward() await self.async_publ_mqtt() @@ -100,9 +100,9 @@ class AsyncStream(): else: raise RuntimeError("Peer closed.") - async def __async_write(self) -> None: + async def async_write(self, headline='Transmit to ') -> None: if self._send_buffer: - hex_dump_memory(logging.INFO, f'Transmit to {self.addr}:', + hex_dump_memory(logging.INFO, f'{headline}{self.addr}:', self._send_buffer, len(self._send_buffer)) self.writer.write(self._send_buffer) await self.writer.drain() @@ -114,7 +114,7 @@ class AsyncStream(): await self.async_create_remote() if self.remoteStream: if self.remoteStream._init_new_client_conn(): - await self.remoteStream.__async_write() + await self.remoteStream.async_write() if self.remoteStream: self.remoteStream._update_header(self._forward_buffer) From fdedfcbf8e438f0ff8c897018b4d7438797d63e1 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 3 May 2024 18:21:59 +0200 Subject: [PATCH 09/29] reneme Modbus constants --- app/src/modbus.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/modbus.py b/app/src/modbus.py index 9745f23..ebaa365 100644 --- a/app/src/modbus.py +++ b/app/src/modbus.py @@ -21,8 +21,12 @@ CRC_INIT = 0xFFFF class Modbus(metaclass=Singleton): - MB_WRITE_SINGLE_REG = 6 - MB_READ_SINGLE_REG = 3 + + READ_REGS = 3 + READ_INPUTS = 4 + WRITE_SINGLE_REG = 6 + '''Modbus function codes''' + __crc_tab = [] def __init__(self): From f78d4ac31075cba554be4c3e8638bde54076e136 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 3 May 2024 18:22:31 +0200 Subject: [PATCH 10/29] remove flush_send_msg() --- app/src/gen3/connection_g3.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/gen3/connection_g3.py b/app/src/gen3/connection_g3.py index 3ac1e7c..c93156e 100644 --- a/app/src/gen3/connection_g3.py +++ b/app/src/gen3/connection_g3.py @@ -29,11 +29,6 @@ class ConnectionG3(AsyncStream, Talent): async def async_publ_mqtt(self) -> None: pass - async def flush_send_msg(self) -> None: - self.writer.write(self._send_buffer) - await self.writer.drain() - self._send_buffer = bytearray(0) - ''' Our private methods ''' From a2f67e7d3e58f2c17ac2a1a2eebef49a37bf306f Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 3 May 2024 18:23:08 +0200 Subject: [PATCH 11/29] use async_write() instead of flush_send_msg() --- app/src/gen3/talent.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index f983c79..ac63cef 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -3,7 +3,6 @@ import logging import time from datetime import datetime - if __name__ == "app.src.gen3.talent": from app.src.messages import hex_dump_memory, Message from app.src.modbus import Modbus @@ -44,6 +43,7 @@ class Talent(Message): self.contact_name = b'' self.contact_mail = b'' self.db = InfosG3() + self.mb = Modbus() self.forward_modbus_rep = False self.switch = { 0x00: self.msg_contact_info, @@ -55,7 +55,6 @@ class Talent(Message): # 0x78: 0x04: self.msg_inverter_data, } - self.mb = Modbus() ''' Our puplic methods @@ -126,14 +125,12 @@ class Talent(Message): async def send_modbus_cmd(self, func, addr, val) -> None: self.forward_modbus_rep = False self.__build_header(0x70, 0x77) - self._send_buffer += b'\x00\x01\xa3\x28' + self._send_buffer += b'\x00\x01\xa3\x28' # fixme modbus_msg = self.mb.build_msg(1, func, addr, val) self._send_buffer += struct.pack('!B', len(modbus_msg)) self._send_buffer += modbus_msg - _len = self.__finish_send_msg() - hex_dump_memory(logging.INFO, 'Send Modbus Command:', - self._send_buffer[self.send_msg_ofs:], _len) - await self.flush_send_msg() + self.__finish_send_msg() + await self.async_write('Send Modbus Command:') def _init_new_client_conn(self) -> bool: contact_name = self.contact_name @@ -220,10 +217,9 @@ class Talent(Message): logger.info(self.__flow_str(self.server_side, 'tx') + f' Ctl: {int(ctrl):#02x} Msg: {fnc.__name__!r}') - def __finish_send_msg(self) -> int: + def __finish_send_msg(self) -> None: _len = len(self._send_buffer) - self.send_msg_ofs struct.pack_into('!l', self._send_buffer, self.send_msg_ofs, _len-4) - return _len def __dispatch_msg(self) -> None: fnc = self.switch.get(self.msg_id, self.msg_unknown) @@ -375,11 +371,11 @@ class Talent(Message): msg_hdr_len = 5 - result = struct.unpack_from('!lB', self._recv_buffer, - self.header_len + 4) + result = struct.unpack_from('!lBB', self._recv_buffer, + self.header_len) modbus_len = result[1] logger.debug(f'Ref: {result[0]}') - logger.debug(f'Modbus Len: {modbus_len}') + logger.debug(f'Modbus MsgLen: {modbus_len} Func:{result[2]}') # logger.info(f'time: {datetime.utcfromtimestamp(result[2]).strftime( # "%Y-%m-%d %H:%M:%S")}') return msg_hdr_len, modbus_len From 763af8b4cfcff38441fdff78b89843c44f6d3a56 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 3 May 2024 18:24:06 +0200 Subject: [PATCH 12/29] add send_modbus_cmd() --- app/src/gen3plus/solarman_v5.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index d23efa3..83f4d49 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -6,12 +6,14 @@ from datetime import datetime if __name__ == "app.src.gen3plus.solarman_v5": from app.src.messages import hex_dump_memory, Message + from app.src.modbus import Modbus from app.src.config import Config from app.src.gen3plus.infos_g3p import InfosG3P from app.src.infos import Register else: # pragma: no cover from messages import hex_dump_memory, Message from config import Config + from modbus import Modbus from gen3plus.infos_g3p import InfosG3P from infos import Register # import traceback @@ -56,6 +58,8 @@ class SolarmanV5(Message): self.snr = 0 self.db = InfosG3P() self.time_ofs = 0 + self.mb = Modbus() + self.forward_modbus_rep = False self.switch = { 0x4210: self.msg_data_ind, # real time data @@ -293,6 +297,14 @@ class SolarmanV5(Message): self._heartbeat()) self.__finish_send_msg() + async def send_modbus_cmd(self, func, addr, val) -> None: + self.forward_modbus_rep = False + self.__build_header(0x4510) + self._send_buffer += struct.pack(' None: self.__build_header(0x4510) self._send_buffer += struct.pack(f' Date: Fri, 3 May 2024 18:24:48 +0200 Subject: [PATCH 13/29] use async_write instead of flush_send_msg() --- app/tests/test_talent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/tests/test_talent.py b/app/tests/test_talent.py index fc4ed4e..7c996ab 100644 --- a/app/tests/test_talent.py +++ b/app/tests/test_talent.py @@ -55,7 +55,7 @@ class MemoryStream(Talent): self.msg_count += 1 return - async def flush_send_msg(self): + async def async_write(self, headline=''): pass @@ -857,7 +857,7 @@ async def test_msg_build_modbus_req(ConfigTsunInv1, MsgModbusCmd): ConfigTsunInv1 m = MemoryStream(b'', (0,), False) m.id_str = b"R170000000000001" - await m.send_modbus_cmd(Modbus.MB_WRITE_SINGLE_REG, 0x2008, 0) + await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0) assert 0==m.send_msg_ofs assert m._forward_buffer==b'' assert m._send_buffer==MsgModbusCmd From 3dbcee63f6907ac08bd6e95fba55d6d1e68198ec Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 3 May 2024 18:25:37 +0200 Subject: [PATCH 14/29] add Modbus topics --- app/src/mqtt.py | 60 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/app/src/mqtt.py b/app/src/mqtt.py index 6a69c95..9d6e83e 100644 --- a/app/src/mqtt.py +++ b/app/src/mqtt.py @@ -59,7 +59,9 @@ class Mqtt(metaclass=Singleton): interval = 5 # Seconds ha_status_topic = f"{ha['auto_conf_prefix']}/status" - inv_cnf_topic = "tsun/+/test" + mb_rated_topic = "tsun/+/rated_load" # fixme + mb_reads_topic = "tsun/+/modbus_read_regs" # fixme + mb_inputs_topic = "tsun/+/modbus_read_inputs" # fixme while True: try: @@ -71,7 +73,9 @@ class Mqtt(metaclass=Singleton): # async with self.__client.messages() as messages: await self.__client.subscribe(ha_status_topic) - await self.__client.subscribe(inv_cnf_topic) + await self.__client.subscribe(mb_rated_topic) + await self.__client.subscribe(mb_reads_topic) + await self.__client.subscribe(mb_inputs_topic) async for message in self.__client.messages: if message.topic.matches(ha_status_topic): @@ -82,20 +86,18 @@ class Mqtt(metaclass=Singleton): self.ha_restarts += 1 await self.__cb_MqttIsUp() - if message.topic.matches(inv_cnf_topic): - topic = str(message.topic) - node_id = topic.split('/')[1] + '/' - payload = message.payload.decode("UTF-8") - logger_mqtt.info(f'InvCnf: {node_id}:{payload}') - for m in Message: - if m.server_side and m.node_id == node_id: - logger_mqtt.info(f'Found: {node_id}') - fnc = getattr(m, "send_modbus_cmd", None) - if callable(fnc): - # await fnc(Modbus.MB_WRITE_SINGLE_REG, - # 0x2008, 2) - await fnc(Modbus.MB_READ_SINGLE_REG, - 0x2008, 1) + if message.topic.matches(mb_rated_topic): + await self.modbus_cmd(message, + Modbus.WRITE_SINGLE_REG, + 1, 0x2008) + + if message.topic.matches(mb_reads_topic): + await self.modbus_cmd(message, + Modbus.READ_REGS, 2) + + if message.topic.matches(mb_inputs_topic): + await self.modbus_cmd(message, + Modbus.READ_INPUTS, 2) except aiomqtt.MqttError: if Config.is_default('mqtt'): @@ -114,7 +116,31 @@ class Mqtt(metaclass=Singleton): self.__client = None return except Exception: - # self.inc_counter('SW_Exception') + # self.inc_counter('SW_Exception') # fixme logger_mqtt.error( f"Exception:\n" f"{traceback.format_exc()}") + + async def modbus_cmd(self, message, func, params=0, addr=0, val=0): + topic = str(message.topic) + node_id = topic.split('/')[1] + '/' + # refactor into a loop over a table + payload = message.payload.decode("UTF-8") + logger_mqtt.info(f'InvCnf: {node_id}:{payload}') + for m in Message: + if m.server_side and m.node_id == node_id: + logger_mqtt.info(f'Found: {node_id}') + fnc = getattr(m, "send_modbus_cmd", None) + res = payload.split(',') + if params != len(res): + logger_mqtt.error(f'Parameter expected: {params}, ' + f'got: {len(res)}') + return + + if callable(fnc): + if params == 1: + val = int(payload) + elif params == 2: + addr = int(res[0], base=16) + val = int(res[1]) # lenght + await fnc(func, addr, val) From eda8ef1db634f030c2a3a8ab3d0d730f9e0e14e1 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 5 May 2024 20:13:51 +0200 Subject: [PATCH 15/29] add Modbus and AT command handler --- app/src/gen3plus/solarman_v5.py | 101 +++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 28 deletions(-) diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index 83f4d49..a459e89 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -48,6 +48,8 @@ class Sequence(): class SolarmanV5(Message): + AT_CMD = 1 + MB_RTU_CMD = 2 def __init__(self, server_side: bool): super().__init__(server_side) @@ -59,7 +61,7 @@ class SolarmanV5(Message): self.db = InfosG3P() self.time_ofs = 0 self.mb = Modbus() - self.forward_modbus_rep = False + self.forward_modbus_resp = False self.switch = { 0x4210: self.msg_data_ind, # real time data @@ -88,7 +90,7 @@ class SolarmanV5(Message): # # MODbus or AT cmd 0x4510: self.msg_command_req, # from server - 0x1510: self.msg_response, # from inverter + 0x1510: self.msg_command_rsp, # from inverter } ''' @@ -298,23 +300,48 @@ class SolarmanV5(Message): self.__finish_send_msg() async def send_modbus_cmd(self, func, addr, val) -> None: - self.forward_modbus_rep = False + self.forward_modbus_resp = False self.__build_header(0x4510) - self._send_buffer += struct.pack(' None: + async def send_at_cmd(self, AT_cmd: str) -> None: self.__build_header(0x4510) - self._send_buffer += struct.pack(f'> 8 @@ -325,21 +352,7 @@ class SolarmanV5(Message): self.new_data[key] = True if inv_update: - db = self.db - MaxPow = db.get_db_value(Register.MAX_DESIGNED_POWER, 0) - Rated = db.get_db_value(Register.RATED_POWER, 0) - Model = None - if MaxPow == 2000: - if Rated == 800 or Rated == 600: - Model = f'TSOL-MS{MaxPow}({Rated})' - else: - Model = f'TSOL-MS{MaxPow}' - elif MaxPow == 1800 or MaxPow == 1600: - Model = f'TSOL-MS{MaxPow}' - if Model: - logger.info(f'Model: {Model}') - self.db.set_db_def_value(Register.EQUIPMENT_MODEL, Model) - + self.__build_model_name() ''' Message handler methods ''' @@ -402,11 +415,42 @@ class SolarmanV5(Message): data = self._recv_buffer[self.header_len:] result = struct.unpack_from(' 4: + logger.info(f'first byte modbus:{data[14]}') + inv_update = False + for key, update in self.mb.recv_resp(self.db, data[14:-2]): + if update: + if key == 'inverter': + inv_update = True + self.new_data[key] = True + + if inv_update: + self.__build_model_name() + + if not self.forward_modbus_resp: + return + self.__forward_msg() + def msg_hbeat_ind(self): data = self._recv_buffer[self.header_len:] result = struct.unpack_from(' Date: Sun, 5 May 2024 20:14:51 +0200 Subject: [PATCH 16/29] add modbus resp handler --- app/src/gen3/talent.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index ac63cef..6225aa0 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -44,7 +44,7 @@ class Talent(Message): self.contact_mail = b'' self.db = InfosG3() self.mb = Modbus() - self.forward_modbus_rep = False + self.forward_modbus_resp = False self.switch = { 0x00: self.msg_contact_info, 0x13: self.msg_ota_update, @@ -123,14 +123,17 @@ class Talent(Message): return async def send_modbus_cmd(self, func, addr, val) -> None: - self.forward_modbus_rep = False + self.forward_modbus_resp = False self.__build_header(0x70, 0x77) - self._send_buffer += b'\x00\x01\xa3\x28' # fixme - modbus_msg = self.mb.build_msg(1, func, addr, val) + self._send_buffer += b'\x00\x01\xa3\x28' # fixme + modbus_msg = self.mb.build_msg(Modbus.INV_ADDR, func, addr, val) self._send_buffer += struct.pack('!B', len(modbus_msg)) self._send_buffer += modbus_msg self.__finish_send_msg() - await self.async_write('Send Modbus Command:') + try: + await self.async_write('Send Modbus Command:') + except Exception: + self._send_buffer = bytearray(0) def _init_new_client_conn(self) -> bool: contact_name = self.contact_name @@ -384,10 +387,16 @@ class Talent(Message): hdr_len, modbus_len = self.parse_modbus_header() if self.ctrl.is_req(): - self.forward_modbus_rep = True + self.forward_modbus_resp = True self.inc_counter('Modbus_Command') elif self.ctrl.is_ind(): - if not self.forward_modbus_rep: + logger.debug(f'Modbus Ind MsgLen: {modbus_len}') + for key, update in self.mb.recv_resp(self.db, self._recv_buffer[ + self.header_len + hdr_len:]): + if update: + self.new_data[key] = True + + if not self.forward_modbus_resp: return else: logger.warning('Unknown Ctrl') From 808bf2fe873642149c7d187e07211b0b8381c20a Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 5 May 2024 20:15:36 +0200 Subject: [PATCH 17/29] add MQTT topic for AT commands --- app/src/mqtt.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/app/src/mqtt.py b/app/src/mqtt.py index 9d6e83e..7257038 100644 --- a/app/src/mqtt.py +++ b/app/src/mqtt.py @@ -62,6 +62,7 @@ class Mqtt(metaclass=Singleton): mb_rated_topic = "tsun/+/rated_load" # fixme mb_reads_topic = "tsun/+/modbus_read_regs" # fixme mb_inputs_topic = "tsun/+/modbus_read_inputs" # fixme + mb_at_cmd_topic = "tsun/+/at_cmd" # fixme while True: try: @@ -76,6 +77,7 @@ class Mqtt(metaclass=Singleton): await self.__client.subscribe(mb_rated_topic) await self.__client.subscribe(mb_reads_topic) await self.__client.subscribe(mb_inputs_topic) + await self.__client.subscribe(mb_at_cmd_topic) async for message in self.__client.messages: if message.topic.matches(ha_status_topic): @@ -99,6 +101,9 @@ class Mqtt(metaclass=Singleton): await self.modbus_cmd(message, Modbus.READ_INPUTS, 2) + if message.topic.matches(mb_at_cmd_topic): + await self.at_cmd(message) + except aiomqtt.MqttError: if Config.is_default('mqtt'): logger_mqtt.info( @@ -116,11 +121,26 @@ class Mqtt(metaclass=Singleton): self.__client = None return except Exception: - # self.inc_counter('SW_Exception') # fixme + # self.inc_counter('SW_Exception') # fixme logger_mqtt.error( f"Exception:\n" f"{traceback.format_exc()}") + def each_inverter(self, message, func_name: str): + topic = str(message.topic) + node_id = topic.split('/')[1] + '/' + for m in Message: + if m.server_side and m.node_id == node_id: + logger_mqtt.debug(f'Found: {node_id}') + fnc = getattr(m, func_name, None) + if callable(fnc): + yield fnc + else: + logger_mqtt.warning(f'Cmd not supported by: {node_id}') + + else: + logger_mqtt.warning(f'Node_id: {node_id} not found') + async def modbus_cmd(self, message, func, params=0, addr=0, val=0): topic = str(message.topic) node_id = topic.split('/')[1] + '/' @@ -144,3 +164,8 @@ class Mqtt(metaclass=Singleton): addr = int(res[0], base=16) val = int(res[1]) # lenght await fnc(func, addr, val) + + async def at_cmd(self, message): + payload = message.payload.decode("UTF-8") + for fnc in self.each_inverter(message, "send_at_cmd"): + await fnc(payload) From 283ae31af2f4e4c55b162ffa5b18041352031fdf Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 5 May 2024 20:16:28 +0200 Subject: [PATCH 18/29] parse modbus message and store values in db --- app/src/modbus.py | 112 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 5 deletions(-) diff --git a/app/src/modbus.py b/app/src/modbus.py index ebaa365..0f16705 100644 --- a/app/src/modbus.py +++ b/app/src/modbus.py @@ -1,9 +1,11 @@ import struct +import logging +from typing import Generator if __name__ == "app.src.modbus": - from app.src.singleton import Singleton + from app.src.infos import Register else: # pragma: no cover - from singleton import Singleton + from infos import Register ####### # TSUN uses the Modbus in the RTU transmission mode. @@ -20,23 +22,123 @@ CRC_POLY = 0xA001 # (LSBF/reverse) CRC_INIT = 0xFFFF -class Modbus(metaclass=Singleton): - +class Modbus(): + INV_ADDR = 1 READ_REGS = 3 READ_INPUTS = 4 WRITE_SINGLE_REG = 6 '''Modbus function codes''' __crc_tab = [] + map = { + 0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501 + 0x420100c0: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501 + 0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'v{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501 + 0x3009: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 + 0x300a: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 + 0x300b: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 + 0x300c: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'eval': 'result-40'}, # noqa: E501 + 0x300e: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501 + 0x300f: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 + 0x3010: {'reg': Register.PV1_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 + 0x3011: {'reg': Register.PV1_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 + 0x3012: {'reg': Register.PV1_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 + 0x3013: {'reg': Register.PV2_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 + 0x3014: {'reg': Register.PV2_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 + 0x3015: {'reg': Register.PV2_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 + 0x3016: {'reg': Register.PV3_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 + 0x3017: {'reg': Register.PV3_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 + 0x3018: {'reg': Register.PV3_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 + 0x3019: {'reg': Register.PV4_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 + 0x301a: {'reg': Register.PV4_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 + 0x301b: {'reg': Register.PV4_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 + 0x301c: {'reg': Register.DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 + 0x420100fa: {'reg': Register.TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 + 0x301f: {'reg': Register.PV1_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 + 0x42010100: {'reg': Register.PV1_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 + 0x3022: {'reg': Register.PV2_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 + 0x42010106: {'reg': Register.PV2_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 + 0x3025: {'reg': Register.PV3_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 + 0x4201010c: {'reg': Register.PV3_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 + 0x3028: {'reg': Register.PV4_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 + 0x42010112: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 + } def __init__(self): - self.__build_crc_tab(CRC_POLY) + if not len(self.__crc_tab): + self.__build_crc_tab(CRC_POLY) + self.last_fcode = 0 + self.last_len = 0 + self.last_reg = 0 def build_msg(self, addr, func, reg, val): msg = struct.pack('>BBHH', addr, func, reg, val) msg += struct.pack(' bool: + logging.info(f'recv_req: first byte modbus:{buf[0]} len:{len(buf)}') + if not self.check_crc(buf): + logging.error('Modbus: CRC error') + return False + if buf[0] != self.INV_ADDR: + logging.info(f'Modbus: Wrong addr{buf[0]}') + return False + res = struct.unpack_from('>BHH', buf, 1) + self.last_fcode = res[0] + self.last_reg = res[1] + self.last_len = res[2] + return True + + def recv_resp(self, info_db, buf: bytearray) -> Generator[tuple[str, bool], + None, None]: + logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}') + if not self.check_crc(buf): + logging.error('Modbus: CRC error') + return + if buf[0] != self.INV_ADDR: + logging.info(f'Modbus: Wrong addr {buf[0]}') + return + if buf[1] != self.last_fcode: + logging.info(f'Modbus: Wrong fcode {buf[1]} != {self.last_fcode}') + return + elmlen = buf[2] >> 1 + if elmlen != self.last_len: + logging.info(f'Modbus: len error {elmlen} != {self.last_len}') + return + + for i in range(0, elmlen): + val = struct.unpack_from('>H', buf, 3+2*i) + addr = self.last_reg+i + # logging.info(f'Modbus: 0x{addr:04x}: {val[0]}') + if addr in self.map: + row = self.map[addr] + info_id = row['reg'] + result = val[0] + # fmt = row['fmt'] + # res = struct.unpack_from(fmt, buf, addr) + # result = res[0] + + if 'eval' in row: + result = eval(row['eval']) + if 'ratio' in row: + result = round(result * row['ratio'], 2) + + keys, level, unit, must_incr = info_db._key_obj(info_id) + + if keys: + name, update = info_db.update_db(keys, must_incr, result) + yield keys[0], update + else: + name = str(f'info-id.0x{addr:x}') + update = False + + info_db.tracer.log(level, f'GEN3PLUS: {name} : {result}{unit}' + f' update: {update}') + def check_crc(self, msg) -> bool: return 0 == self.__calc_crc(msg) From 5822f5de50feb8aa564b7ca43a8669a8409d6ccc Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 5 May 2024 20:18:19 +0200 Subject: [PATCH 19/29] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ddf9b0..0eede6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- parse Modbus values and store them in the database +- add cron task to request the output power every minute +- GEN3PLUS: add MQTT topics to send AT commands to the inverter +- add MQTT topics to send Modbus commands to the inverter - convert data collect interval to minutes - add postfix for rc and dev versions to the version number - change logging level to DEBUG for some logs From 29ee540a19c3b00814e796a7e7225a71dcff0499 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 5 May 2024 20:18:45 +0200 Subject: [PATCH 20/29] add cron tasks for modbus requests every minute --- app/src/scheduler.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/scheduler.py b/app/src/scheduler.py index b5d238d..dc45890 100644 --- a/app/src/scheduler.py +++ b/app/src/scheduler.py @@ -3,6 +3,8 @@ import json from mqtt import Mqtt from aiocron import crontab from infos import ClrAtMidnight +from modbus import Modbus +from messages import Message logger_mqtt = logging.getLogger('mqtt') @@ -17,7 +19,9 @@ class Schedule: cls.mqtt = Mqtt(None) crontab('0 0 * * *', func=cls.atmidnight, start=True) - # crontab('*/5 * * * *', func=cls.atmidnight, start=True) + + # every minute + crontab('* * * * *', func=cls.regular_modbus_cmds, start=True) @classmethod async def atmidnight(cls) -> None: @@ -28,3 +32,12 @@ class Schedule: logger_mqtt.debug(f'{key}: {data}') data_json = json.dumps(data) await cls.mqtt.publish(f"{key}", data_json) + + @classmethod + async def regular_modbus_cmds(cls): + # logging.info("Regular Modbus requests") + for m in Message: + if m.server_side: + fnc = getattr(m, "send_modbus_cmd", None) + if callable(fnc): + await fnc(Modbus.READ_REGS, 0x300e, 2) From bf0f152d5a26ac79e8549fb50d136fd8a0955ce1 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 5 May 2024 20:20:19 +0200 Subject: [PATCH 21/29] add unit tests for modbus --- app/tests/test_modbus.py | 20 ++++++++++++++++++++ app/tests/test_solarman.py | 21 ++++++++++++++------- app/tests/test_talent.py | 4 ++-- system_tests/test_tcp_socket.py | 2 +- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/app/tests/test_modbus.py b/app/tests/test_modbus.py index 0e9cf5b..fcec232 100644 --- a/app/tests/test_modbus.py +++ b/app/tests/test_modbus.py @@ -1,7 +1,12 @@ # test_with_pytest.py # import pytest, logging from app.src.modbus import Modbus +from app.src.infos import Infos +class TestHelper(Modbus): + def __init__(self): + super().__init__() + self.db = Infos() def test_modbus_crc(): mb = Modbus() @@ -19,3 +24,18 @@ def test_build_modbus_pdu(): assert pdu == b'\x01\x06\x20\x00\x00\x12\x02\x07' assert mb.check_crc(pdu) +def test_build_recv(): + mb = TestHelper() + pdu = mb.build_msg(1,3,0x300e,0x2) + assert pdu == b'\x01\x03\x30\x0e\x00\x02\xaa\xc8' + assert mb.check_crc(pdu) + call = 0 + for key, update in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4'): + if key == 'grid': + assert update == True + elif key == 'inverter': + assert update == True + else: + assert False + call += 1 + assert 2 == call diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py index 48f5509..e699ae7 100644 --- a/app/tests/test_solarman.py +++ b/app/tests/test_solarman.py @@ -6,6 +6,9 @@ from app.src.gen3plus.solarman_v5 import SolarmanV5 from app.src.config import Config from app.src.infos import Infos, Register + +pytest_plugins = ('pytest_asyncio',) + # initialize the proxy statistics Infos.static_init() @@ -54,6 +57,9 @@ class MemoryStream(SolarmanV5): pass return copied_bytes + async def async_write(self, headline=''): + pass + def _SolarmanV5__flush_recv_msg(self) -> None: super()._SolarmanV5__flush_recv_msg() self.msg_count += 1 @@ -725,7 +731,7 @@ def test_device_rsp(ConfigTsunInv1, DeviceRspMsg): assert m.data_len == 0x0a assert m._recv_buffer==b'' assert m._send_buffer==b'' - assert m._forward_buffer==b'' # DeviceRspMsg + assert m._forward_buffer==DeviceRspMsg assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 m.close() @@ -743,7 +749,7 @@ def test_inverter_rsp(ConfigTsunInv1, InverterRspMsg): assert m.data_len == 0x0a assert m._recv_buffer==b'' assert m._send_buffer==b'' - assert m._forward_buffer==b'' # InverterRspMsg + assert m._forward_buffer==InverterRspMsg assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 m.close() @@ -779,7 +785,7 @@ def test_heartbeat_rsp(ConfigTsunInv1, HeartbeatRspMsg): assert m.data_len == 0x0a assert m._recv_buffer==b'' assert m._send_buffer==b'' - assert m._forward_buffer==b'' # HeartbeatRspMsg + assert m._forward_buffer==HeartbeatRspMsg assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 m.close() @@ -820,7 +826,7 @@ def test_sync_start_rsp(ConfigTsunInv1, SyncStartRspMsg): assert m.data_len == 0x0a assert m._recv_buffer==b'' assert m._send_buffer==b'' - assert m._forward_buffer==b'' # HeartbeatRspMsg + assert m._forward_buffer==SyncStartRspMsg assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 m.close() @@ -856,7 +862,7 @@ def test_sync_end_rsp(ConfigTsunInv1, SyncEndRspMsg): assert m.data_len == 0x0a assert m._recv_buffer==b'' assert m._send_buffer==b'' - assert m._forward_buffer==b'' # HeartbeatRspMsg + assert m._forward_buffer==SyncEndRspMsg assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 m.close() @@ -942,7 +948,8 @@ def test_build_logger_modell(ConfigTsunAllowAll, DeviceIndMsg): 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): +@pytest.mark.asyncio +async def test_AT_cmd(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, AtCommandIndMsg): ConfigTsunAllowAll m = MemoryStream(DeviceIndMsg, (0,), True) m.read() @@ -954,7 +961,7 @@ def test_AT_cmd(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, AtCommandIndMsg) 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') + await 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'' diff --git a/app/tests/test_talent.py b/app/tests/test_talent.py index 7c996ab..4b1de2f 100644 --- a/app/tests/test_talent.py +++ b/app/tests/test_talent.py @@ -795,7 +795,7 @@ def test_msg_modbus_rsp1(ConfigTsunInv1, MsgModbusRsp): m = MemoryStream(MsgModbusRsp, (0,), False) m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Modbus_Command'] = 0 - m.forward_modbus_rep = False + m.forward_modbus_resp = 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 @@ -816,7 +816,7 @@ def test_msg_modbus_rsp2(ConfigTsunInv1, MsgModbusRsp): m = MemoryStream(MsgModbusRsp, (0,), False) m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Modbus_Command'] = 0 - m.forward_modbus_rep = True + m.forward_modbus_resp = True 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 diff --git a/system_tests/test_tcp_socket.py b/system_tests/test_tcp_socket.py index 606ea68..f01a0a0 100644 --- a/system_tests/test_tcp_socket.py +++ b/system_tests/test_tcp_socket.py @@ -224,7 +224,7 @@ def test_send_inv_data(ClientConnection, MsgTimeStampReq, MsgTimeStampResp, MsgI data = s.recv(1024) except TimeoutError: pass - # time.sleep(32.5) + time.sleep(32.5) # assert data == MsgTimeStampResp try: s.sendall(MsgInvData) From f804b755a4752732e5a5753002d000707356bfe0 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Mon, 6 May 2024 23:18:47 +0200 Subject: [PATCH 22/29] improve modbus trace --- app/src/gen3/talent.py | 2 +- app/src/gen3plus/solarman_v5.py | 5 +++-- app/src/modbus.py | 9 +++++---- app/tests/test_modbus.py | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index 6225aa0..2c70062 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -392,7 +392,7 @@ class Talent(Message): elif self.ctrl.is_ind(): logger.debug(f'Modbus Ind MsgLen: {modbus_len}') for key, update in self.mb.recv_resp(self.db, self._recv_buffer[ - self.header_len + hdr_len:]): + self.header_len + hdr_len:], self.new_data): if update: self.new_data[key] = True diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index a459e89..5805361 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -434,11 +434,12 @@ class SolarmanV5(Message): elif ftype == self.MB_RTU_CMD: valid = data[1] modbus_msg_len = self.data_len - 14 - logger.info(f'modbus_len:{modbus_msg_len} accepted:{valid}') + logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}') if valid == 1 and modbus_msg_len > 4: logger.info(f'first byte modbus:{data[14]}') inv_update = False - for key, update in self.mb.recv_resp(self.db, data[14:-2]): + for key, update in self.mb.recv_resp(self.db, data[14:-2], + self.node_id): if update: if key == 'inverter': inv_update = True diff --git a/app/src/modbus.py b/app/src/modbus.py index 0f16705..37eadff 100644 --- a/app/src/modbus.py +++ b/app/src/modbus.py @@ -93,8 +93,8 @@ class Modbus(): self.last_len = res[2] return True - def recv_resp(self, info_db, buf: bytearray) -> Generator[tuple[str, bool], - None, None]: + def recv_resp(self, info_db, buf: bytearray, node_id: str) -> \ + Generator[tuple[str, bool], None, None]: logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}') if not self.check_crc(buf): logging.error('Modbus: CRC error') @@ -136,8 +136,9 @@ class Modbus(): name = str(f'info-id.0x{addr:x}') update = False - info_db.tracer.log(level, f'GEN3PLUS: {name} : {result}{unit}' - f' update: {update}') + info_db.tracer.log(level, + f'MODBUS({node_id}): {name} : {result}' + f'{unit} update: {update}') def check_crc(self, msg) -> bool: return 0 == self.__calc_crc(msg) diff --git a/app/tests/test_modbus.py b/app/tests/test_modbus.py index fcec232..b1764e9 100644 --- a/app/tests/test_modbus.py +++ b/app/tests/test_modbus.py @@ -30,7 +30,7 @@ def test_build_recv(): assert pdu == b'\x01\x03\x30\x0e\x00\x02\xaa\xc8' assert mb.check_crc(pdu) call = 0 - for key, update in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4'): + for key, update in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf4', 'test'): if key == 'grid': assert update == True elif key == 'inverter': From 54d2bf4439eb5b9ae9e5df12397cfdd520ae560f Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 7 May 2024 17:52:51 +0200 Subject: [PATCH 23/29] set err value for unit tests --- app/src/modbus.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/app/src/modbus.py b/app/src/modbus.py index 37eadff..e8a8d0e 100644 --- a/app/src/modbus.py +++ b/app/src/modbus.py @@ -32,12 +32,13 @@ class Modbus(): __crc_tab = [] map = { 0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501 - 0x420100c0: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501 + # 0x????: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501 0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'v{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501 0x3009: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 0x300a: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 0x300b: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 0x300c: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'eval': 'result-40'}, # noqa: E501 + # 0x300d 0x300e: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501 0x300f: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 0x3010: {'reg': Register.PV1_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 @@ -53,15 +54,15 @@ class Modbus(): 0x301a: {'reg': Register.PV4_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 0x301b: {'reg': Register.PV4_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 0x301c: {'reg': Register.DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 - 0x420100fa: {'reg': Register.TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 + # 0x301d: {'reg': Register.TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 0x301f: {'reg': Register.PV1_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 - 0x42010100: {'reg': Register.PV1_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 + # 0x3020: {'reg': Register.PV1_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 0x3022: {'reg': Register.PV2_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 - 0x42010106: {'reg': Register.PV2_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 + # 0x3023: {'reg': Register.PV2_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 0x3025: {'reg': Register.PV3_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 - 0x4201010c: {'reg': Register.PV3_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 + # 0x3026: {'reg': Register.PV3_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 0x3028: {'reg': Register.PV4_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 - 0x42010112: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 + # 0x3029: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 } def __init__(self): @@ -70,6 +71,7 @@ class Modbus(): self.last_fcode = 0 self.last_len = 0 self.last_reg = 0 + self.err = 0 def build_msg(self, addr, func, reg, val): msg = struct.pack('>BBHH', addr, func, reg, val) @@ -77,38 +79,47 @@ class Modbus(): self.last_fcode = func self.last_reg = reg self.last_len = val + self.err = 0 return msg def recv_req(self, buf: bytearray) -> bool: - logging.info(f'recv_req: first byte modbus:{buf[0]} len:{len(buf)}') + # logging.info(f'recv_req: first byte modbus:{buf[0]} len:{len(buf)}') if not self.check_crc(buf): + self.err = 1 logging.error('Modbus: CRC error') return False if buf[0] != self.INV_ADDR: + self.err = 2 logging.info(f'Modbus: Wrong addr{buf[0]}') return False res = struct.unpack_from('>BHH', buf, 1) self.last_fcode = res[0] self.last_reg = res[1] self.last_len = res[2] + self.err = 0 return True def recv_resp(self, info_db, buf: bytearray, node_id: str) -> \ Generator[tuple[str, bool], None, None]: - logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}') + # logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}') if not self.check_crc(buf): logging.error('Modbus: CRC error') + self.err = 1 return if buf[0] != self.INV_ADDR: logging.info(f'Modbus: Wrong addr {buf[0]}') + self.err = 2 return if buf[1] != self.last_fcode: logging.info(f'Modbus: Wrong fcode {buf[1]} != {self.last_fcode}') + self.err = 3 return elmlen = buf[2] >> 1 if elmlen != self.last_len: logging.info(f'Modbus: len error {elmlen} != {self.last_len}') + self.err = 4 return + self.err = 0 for i in range(0, elmlen): val = struct.unpack_from('>H', buf, 3+2*i) From d5010fe053894f55ed903eaa6e10593fe81124f2 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 7 May 2024 17:56:54 +0200 Subject: [PATCH 24/29] parse modbus corect if we have received more than one message --- app/src/gen3/talent.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index 2c70062..e6d3b67 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -55,6 +55,7 @@ class Talent(Message): # 0x78: 0x04: self.msg_inverter_data, } + self.modbus_elms = 0 # for unit tests ''' Our puplic methods @@ -377,10 +378,8 @@ class Talent(Message): result = struct.unpack_from('!lBB', self._recv_buffer, self.header_len) modbus_len = result[1] - logger.debug(f'Ref: {result[0]}') - logger.debug(f'Modbus MsgLen: {modbus_len} Func:{result[2]}') - # logger.info(f'time: {datetime.utcfromtimestamp(result[2]).strftime( - # "%Y-%m-%d %H:%M:%S")}') + # logger.debug(f'Ref: {result[0]}') + # logger.debug(f'Modbus MsgLen: {modbus_len} Func:{result[2]}') return msg_hdr_len, modbus_len def msg_modbus(self): @@ -390,11 +389,14 @@ class Talent(Message): self.forward_modbus_resp = True self.inc_counter('Modbus_Command') elif self.ctrl.is_ind(): - logger.debug(f'Modbus Ind MsgLen: {modbus_len}') + # logger.debug(f'Modbus Ind MsgLen: {modbus_len}') + self.modbus_elms = 0 for key, update in self.mb.recv_resp(self.db, self._recv_buffer[ - self.header_len + hdr_len:], self.new_data): + self.header_len + hdr_len:self.header_len+self.data_len], + self.new_data): if update: self.new_data[key] = True + self.modbus_elms += 1 if not self.forward_modbus_resp: return From 39beb0cb44136694c37c114cb270eba2a37e5a9d Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 7 May 2024 18:02:09 +0200 Subject: [PATCH 25/29] add more modbus tests --- app/tests/test_talent.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/app/tests/test_talent.py b/app/tests/test_talent.py index 4b1de2f..cc9ab85 100644 --- a/app/tests/test_talent.py +++ b/app/tests/test_talent.py @@ -200,6 +200,15 @@ def MsgModbusInv(): msg += b'\x00\x00\x03\xc8' return msg +@pytest.fixture +def MsgModbusResp20(): + msg = b'\x00\x00\x00\x45\x10R170000000000001' + msg += b'\x91\x77\x17\x18\x19\x1a\x2d\x01\x03\x28\x51' + msg += b'\x09\x08\xd3\x00\x29\x13\x87\x00\x3e\x00\x00\x01\x2c\x03\xb4\x00' + msg += b'\x08\x00\x00\x00\x00\x01\x59\x01\x21\x03\xe6\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\xdb\x6b' + return msg + def test_read_message(MsgContactInfo): m = MemoryStream(MsgContactInfo, (0,)) m.read() # read complete msg, and dispatch msg @@ -852,6 +861,33 @@ def test_msg_modbus_invalid(ConfigTsunInv1, MsgModbusInv): assert m.db.stat['proxy']['Modbus_Command'] == 0 m.close() +def test_msg_modbus_fragment(ConfigTsunInv1, MsgModbusResp20): + ConfigTsunInv1 + # receive more bytes than expected (7 bytes from the next msg) + m = MemoryStream(MsgModbusResp20+b'\x00\x00\x00\x45\x10\x52\x31', (0,)) + m.db.stat['proxy']['Unknown_Ctrl'] = 0 + m.db.stat['proxy']['Modbus_Command'] = 0 + m.forward_modbus_resp = True + m.mb.last_fcode = 3 + m.mb.last_len = 20 + m.mb.last_reg = 0x3008 + 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.unique_id == 'R170000000000001' + assert int(m.ctrl)==0x91 + assert m.msg_id==119 + assert m.header_len==23 + assert m.data_len==50 + assert m._forward_buffer==MsgModbusResp20 + assert m._send_buffer==b'' + assert m.mb.err == 0 + assert m.modbus_elms == 20-1 # register 0x300d is unknown, so one value can't be mapped + assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 + assert m.db.stat['proxy']['Modbus_Command'] == 0 + m.close() + @pytest.mark.asyncio async def test_msg_build_modbus_req(ConfigTsunInv1, MsgModbusCmd): ConfigTsunInv1 From 02d9f01947f4eea7fbfe2054bb3da8ce1bb08f4c Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 7 May 2024 18:32:56 +0200 Subject: [PATCH 26/29] don't send AT or Modbus cmds on closed connections --- app/src/gen3/talent.py | 2 ++ app/src/gen3plus/solarman_v5.py | 6 ++++-- app/src/mqtt.py | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index e6d3b67..1534446 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -45,6 +45,7 @@ class Talent(Message): self.db = InfosG3() self.mb = Modbus() self.forward_modbus_resp = False + self.closed = False self.switch = { 0x00: self.msg_contact_info, 0x13: self.msg_ota_update, @@ -66,6 +67,7 @@ class Talent(Message): # so we have to erase self.switch, otherwise this instance can't be # deallocated by the garbage collector ==> we get a memory leak self.switch.clear() + self.closed = True def __set_serial_no(self, serial_no: str): diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index 5805361..126e06e 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -62,6 +62,7 @@ class SolarmanV5(Message): self.time_ofs = 0 self.mb = Modbus() self.forward_modbus_resp = False + self.closed = False self.switch = { 0x4210: self.msg_data_ind, # real time data @@ -102,6 +103,7 @@ class SolarmanV5(Message): # so we have to erase self.switch, otherwise this instance can't be # deallocated by the garbage collector ==> we get a memory leak self.switch.clear() + self.closed = True def __set_serial_no(self, snr: int): serial_no = str(snr) @@ -434,9 +436,9 @@ class SolarmanV5(Message): elif ftype == self.MB_RTU_CMD: valid = data[1] modbus_msg_len = self.data_len - 14 - logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}') + # logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}') if valid == 1 and modbus_msg_len > 4: - logger.info(f'first byte modbus:{data[14]}') + # logger.info(f'first byte modbus:{data[14]}') inv_update = False for key, update in self.mb.recv_resp(self.db, data[14:-2], self.node_id): diff --git a/app/src/mqtt.py b/app/src/mqtt.py index 7257038..3469201 100644 --- a/app/src/mqtt.py +++ b/app/src/mqtt.py @@ -130,7 +130,7 @@ class Mqtt(metaclass=Singleton): topic = str(message.topic) node_id = topic.split('/')[1] + '/' for m in Message: - if m.server_side and m.node_id == node_id: + if m.server_side and not m.closed and (m.node_id == node_id): logger_mqtt.debug(f'Found: {node_id}') fnc = getattr(m, func_name, None) if callable(fnc): @@ -148,7 +148,7 @@ class Mqtt(metaclass=Singleton): payload = message.payload.decode("UTF-8") logger_mqtt.info(f'InvCnf: {node_id}:{payload}') for m in Message: - if m.server_side and m.node_id == node_id: + if m.server_side and not m.closed and (m.node_id == node_id): logger_mqtt.info(f'Found: {node_id}') fnc = getattr(m, "send_modbus_cmd", None) res = payload.split(',') From e15387b1ff7dfff1afce02532fa7377b2f52ab91 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 7 May 2024 19:41:07 +0200 Subject: [PATCH 27/29] fix modbus trace --- app/src/gen3/talent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index 1534446..45eb081 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -395,7 +395,7 @@ class Talent(Message): self.modbus_elms = 0 for key, update in self.mb.recv_resp(self.db, self._recv_buffer[ self.header_len + hdr_len:self.header_len+self.data_len], - self.new_data): + self.node_id): if update: self.new_data[key] = True self.modbus_elms += 1 From 3fd528bdbee1fd63b6c80a8524e28b5237c438a7 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 7 May 2024 21:20:12 +0200 Subject: [PATCH 28/29] improve logging --- app/src/gen3/infos_g3.py | 4 ++-- app/src/gen3plus/infos_g3p.py | 4 ++-- app/src/infos.py | 2 +- app/src/modbus.py | 8 ++++---- app/src/scheduler.py | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/gen3/infos_g3.py b/app/src/gen3/infos_g3.py index 7e45634..d3fb987 100644 --- a/app/src/gen3/infos_g3.py +++ b/app/src/gen3/infos_g3.py @@ -161,7 +161,7 @@ class InfosG3(Infos): update = False name = str(f'info-id.0x{addr:x}') - self.tracer.log(level, f'GEN3: {name} : {result}{unit}' - f' update: {update}') + if update: + self.tracer.log(level, f'GEN3: {name} : {result}{unit}') i += 1 diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index b0adc0a..ed8d9bd 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -122,5 +122,5 @@ class InfosG3P(Infos): name = str(f'info-id.0x{addr:x}') update = False - self.tracer.log(level, f'GEN3PLUS: {name} : {result}{unit}' - f' update: {update}') + if update: + self.tracer.log(level, f'GEN3PLUS: {name} : {result}{unit}') diff --git a/app/src/infos.py b/app/src/infos.py index e9e8ebe..dadacd7 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -193,7 +193,7 @@ class Infos: Register.SERIAL_NUMBER: {'name': ['inverter', 'Serial_Number'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 Register.EQUIPMENT_MODEL: {'name': ['inverter', 'Equipment_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 Register.NO_INPUTS: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'fmt': '| string + " W"', 'name': 'Max Designed Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'fmt': '| string + " W"', 'name': 'Max Designed Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'fmt': '| string + " W"', 'name': 'Rated Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV1_MANUFACTURER: {'name': ['inverter', 'PV1_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 diff --git a/app/src/modbus.py b/app/src/modbus.py index e8a8d0e..25c5734 100644 --- a/app/src/modbus.py +++ b/app/src/modbus.py @@ -146,10 +146,10 @@ class Modbus(): else: name = str(f'info-id.0x{addr:x}') update = False - - info_db.tracer.log(level, - f'MODBUS({node_id}): {name} : {result}' - f'{unit} update: {update}') + if update: + info_db.tracer.log(level, + f'MODBUS[{node_id}]: {name} : {result}' + f'{unit}') def check_crc(self, msg) -> bool: return 0 == self.__calc_crc(msg) diff --git a/app/src/scheduler.py b/app/src/scheduler.py index dc45890..a1e763b 100644 --- a/app/src/scheduler.py +++ b/app/src/scheduler.py @@ -40,4 +40,4 @@ class Schedule: if m.server_side: fnc = getattr(m, "send_modbus_cmd", None) if callable(fnc): - await fnc(Modbus.READ_REGS, 0x300e, 2) + await fnc(Modbus.READ_REGS, 0x3008, 20) From 2301511242380ea365494d0c351dab760abc6a4e Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 7 May 2024 22:11:55 +0200 Subject: [PATCH 29/29] update documentation --- CHANGELOG.md | 1 + README.md | 9 +- app/proxy.svg | 466 ++++++++++++++++++++++++++----------------------- app/proxy.yuml | 8 +- 4 files changed, 255 insertions(+), 229 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eede6e..0ec1d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- add timeout monitoring for received packets - parse Modbus values and store them in the database - add cron task to request the output power every minute - GEN3PLUS: add MQTT topics to send AT commands to the inverter diff --git a/README.md b/README.md index 1a26179..342a643 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,15 @@ If you use a Pi-hole, you can also store the host entry in the Pi-hole. ## Features -- supports TSUN GEN3 PLUS inverters: TSOL-MS2000, MS1800 and MS1600 -- supports TSUN GEN3 inverters: TSOL-MS800, MS700, MS600, MS400, MS350 and MS300 +- Supports TSUN GEN3 PLUS inverters: TSOL-MS2000, MS1800 and MS1600 +- Supports TSUN GEN3 inverters: TSOL-MS800, MS700, MS600, MS400, MS350 and MS300 - `MQTT` support - `Home-Assistant` auto-discovery support +- `MODBUS` support via MQTT topics +- `AT Command` support via MQTT topics (GEN3PLUS only) +- Faster DataUp interval sends measurement data to the MQTT broker every minute - Self-sufficient island operation without internet -- runs in a non-root Docker Container +- Runs in a non-root Docker Container ## Home Assistant Screenshots diff --git a/app/proxy.svg b/app/proxy.svg index dfbdd46..cef1e69 100644 --- a/app/proxy.svg +++ b/app/proxy.svg @@ -4,352 +4,372 @@ - - + + G - + A0 - - - -You can stick notes -on diagrams too! + + + +You can stick notes +on diagrams too! A1 - -Singleton + +Singleton A2 - -Mqtt - -<static>ha_restarts -<static>__client -<static>__cb_MqttIsUp - -<async>publish() -<async>close() + +Mqtt + +<static>ha_restarts +<static>__client +<static>__cb_MqttIsUp + +<async>publish() +<async>close() A1->A2 - - - - - -A3 - -Modbus - - - -A1->A3 - - + + A11 - -Inverter - -cls.db_stat -cls.entity_prfx -cls.discovery_prfx -cls.proxy_node_id -cls.proxy_unique_id -cls.mqtt:Mqtt - + +Inverter + +cls.db_stat +cls.entity_prfx +cls.discovery_prfx +cls.proxy_node_id +cls.proxy_unique_id +cls.mqtt:Mqtt + - + A2->A11 - + + + + +A3 + +Modbus + + +build_msg() +recv_req() +recv_resp() +check_crc() A4 - -IterRegistry - - -__iter__ + +IterRegistry + + +__iter__ A5 - -Message - -server_side:bool -header_valid:bool -header_len:unsigned -data_len:unsigned -unique_id -node_id -sug_area -_recv_buffer:bytearray -_send_buffer:bytearray -_forward_buffer:bytearray -db:Infos -new_data:list - -_read():void<abstract> -close():void -inc_counter():void -dec_counter():void + +Message + +server_side:bool +header_valid:bool +header_len:unsigned +data_len:unsigned +unique_id +node_id +sug_area +_recv_buffer:bytearray +_send_buffer:bytearray +_forward_buffer:bytearray +db:Infos +new_data:list + +_read():void<abstract> +close():void +inc_counter():void +dec_counter():void - + A4->A5 - - + + A6 - -Talent - -await_conn_resp_cnt -id_str -contact_name -contact_mail -switch - -msg_contact_info() -msg_ota_update() -msg_get_time() -msg_collector_data() -msg_inverter_data() -msg_unknown() -close() + +Talent + +await_conn_resp_cnt +id_str +contact_name +contact_mail +db:InfosG3 +mb:Modbus +switch + +msg_contact_info() +msg_ota_update() +msg_get_time() +msg_collector_data() +msg_inverter_data() +msg_unknown() +close() - + A5->A6 - - + + A7 - -SolarmanV5 - -control -serial -snr -switch - -msg_unknown() -close() + +SolarmanV5 + +control +serial +snr +db:InfosG3P +mb:Modbus +switch + +msg_unknown() +close() - + A5->A7 - - + + + + + +A6->A3 + + +1 +has A8 - -ConnectionG3 - -remoteStream:ConnectionG3 - -close() + +ConnectionG3 + +remoteStream:ConnectionG3 + +close() - + A6->A8 - - + + + + + +A7->A3 + + +1 +has A9 - -ConnectionG3P - -remoteStream:ConnectionG3P - -close() + +ConnectionG3P + +remoteStream:ConnectionG3P + +close() A7->A9 - - + + - + A8->A8 - - -0..1 -has + + +0..1 +has A12 - -InverterG3 - -__ha_restarts - -async_create_remote() -close() + +InverterG3 + +__ha_restarts + +async_create_remote() +close() - + A8->A12 - - + + - + A9->A9 - - -0..1 -has + + +0..1 +has A13 - -InverterG3P - -__ha_restarts - -async_create_remote() -close() + +InverterG3P + +__ha_restarts + +async_create_remote() +close() - + A9->A13 - - + + A10 - -AsyncStream - -reader -writer -addr -r_addr -l_addr - -<async>server_loop() -<async>client_loop() -<async>loop -disc() -close() -__async_read() -async_write() -__async_forward() + +AsyncStream + +reader +writer +addr +r_addr +l_addr + +<async>server_loop() +<async>client_loop() +<async>loop +disc() +close() +__async_read() +async_write() +__async_forward() - + A10->A8 - - + + - + A10->A9 - - + + - + A11->A12 - - + + - + A11->A13 - - + + A14 - -Infos - -stat -new_stat_data -info_dev - -static_init() -dev_value() -inc_counter() -dec_counter() -ha_proxy_conf -ha_conf -update_db -set_db_def_value -get_db_value -ignore_this_device + +Infos + +stat +new_stat_data +info_dev + +static_init() +dev_value() +inc_counter() +dec_counter() +ha_proxy_conf +ha_conf +update_db +set_db_def_value +get_db_value +ignore_this_device A15 - -InfosG3 - - -ha_confs() -parse() + +InfosG3 + + +ha_confs() +parse() - + A14->A15 - - + + A16 - -InfosG3P - - -ha_confs() -parse() + +InfosG3P + + +ha_confs() +parse() - + A14->A16 - - + + - + A15->A6 - - + + - + A16->A7 - - + + diff --git a/app/proxy.yuml b/app/proxy.yuml index daf5d3c..7514a93 100644 --- a/app/proxy.yuml +++ b/app/proxy.yuml @@ -4,12 +4,14 @@ [note: You can stick notes on diagrams too!{bg:cornsilk}] [Singleton]^[Mqtt|ha_restarts;__client;__cb_MqttIsUp|publish();close()] -[Singleton]^[Modbus] +[Modbus||build_msg();recv_req();recv_resp();check_crc()] [IterRegistry||__iter__]^[Message|server_side:bool;header_valid:bool;header_len:unsigned;data_len:unsigned;unique_id;node_id;sug_area;_recv_buffer:bytearray;_send_buffer:bytearray;_forward_buffer:bytearray;db:Infos;new_data:list|_read():void;close():void;inc_counter():void;dec_counter():void] -[Message]^[Talent|await_conn_resp_cnt;id_str;contact_name;contact_mail;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;close()] -[Message]^[SolarmanV5|control;serial;snr;switch|msg_unknown();;close()] +[Message]^[Talent|await_conn_resp_cnt;id_str;contact_name;contact_mail;db:InfosG3;mb:Modbus;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;close()] +[Message]^[SolarmanV5|control;serial;snr;db:InfosG3P;mb:Modbus;switch|msg_unknown();;close()] [Talent]^[ConnectionG3|remoteStream:ConnectionG3|close()] +[Talent]has-1>[Modbus] [SolarmanV5]^[ConnectionG3P|remoteStream:ConnectionG3P|close()] +[SolarmanV5]has-1>[Modbus] [AsyncStream|reader;writer;addr;r_addr;l_addr|server_loop();client_loop();loop;disc();close();;__async_read();async_write();__async_forward()]^[ConnectionG3] [AsyncStream]^[ConnectionG3P] [Inverter|cls.db_stat;cls.entity_prfx;cls.discovery_prfx;cls.proxy_node_id;cls.proxy_unique_id;cls.mqtt:Mqtt|]^[InverterG3|__ha_restarts|async_create_remote();;close()]