From dc4728122ee5662f3e8ec8b6452f59ac141bb7bd Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Mon, 22 Jul 2024 23:27:17 +0200 Subject: [PATCH] S allius/issue128 (#130) * set Register.NO_INPUTS fix to 4 for GEN3PLUS * don't set Register.NO_INPUTS per MODBUS * fix unit tests * register OUTPUT_COEFFICIENT at HA * update changelog * - Home Assistant: improve inverter status value texts * - GEN3: add inverter status * on closing send outstanding MQTT data to the broker * force MQTT publish on every conn open and close * reset inverter state on close - workaround which reset the inverter status to offline when the inverter has a very low output power on connection close * improve client modified - reduce the polling cadence to 30s - set controller statistics for HA * client mode set controller IP for HA --- CHANGELOG.md | 5 +++++ app/src/async_stream.py | 15 +++++++++++---- app/src/gen3/infos_g3.py | 2 ++ app/src/gen3/talent.py | 12 +++++++++++- app/src/gen3plus/infos_g3p.py | 3 ++- app/src/gen3plus/solarman_v5.py | 23 ++++++++++++++++++----- app/src/infos.py | 22 ++++++++++++++++++---- app/src/modbus.py | 1 - app/src/modbus_tcp.py | 2 +- app/tests/test_infos_g3p.py | 28 +++++++++++++++++++++------- 10 files changed, 89 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f648e3..32c6995 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- Home Assistant: improve inverter status value texts +- GEN3: add inverter status +- fix flapping registers [#128](https://github.com/s-allius/tsun-gen3-proxy/issues/128) +- register OUTPUT_COEFFICIENT at HA +- GEN3: INVERTER_STATUS, - add config option to disable the MODBUS polling [#120](https://github.com/s-allius/tsun-gen3-proxy/issues/120) - make the maximum output coefficient configurable [#123](https://github.com/s-allius/tsun-gen3-proxy/issues/123) - cleanup shutdown diff --git a/app/src/async_stream.py b/app/src/async_stream.py index 41378fd..3082a75 100644 --- a/app/src/async_stream.py +++ b/app/src/async_stream.py @@ -44,13 +44,24 @@ class AsyncStream(): to = self.MAX_CLOUD_IDLE_TIME return to + async def __publish_outstanding_mqtt(self): + '''Publish all outstanding MQTT topics''' + try: + if self.unique_id: + await self.async_publ_mqtt() + await self._async_publ_mqtt_proxy_stat('proxy') + except Exception: + pass + async def server_loop(self, addr: str) -> None: '''Loop for receiving messages from the inverter (server-side)''' logger.info(f'[{self.node_id}:{self.conn_no}] ' f'Accept connection from {addr}') self.inc_counter('Inverter_Cnt') + await self.__publish_outstanding_mqtt() await self.loop() self.dec_counter('Inverter_Cnt') + await self.__publish_outstanding_mqtt() logger.info(f'[{self.node_id}:{self.conn_no}] Server loop stopped for' f' r{self.r_addr}') @@ -61,10 +72,6 @@ class AsyncStream(): f'connection: [{self.remoteStream.node_id}:' f'{self.remoteStream.conn_no}]') await self.remoteStream.disc() - try: - await self._async_publ_mqtt_proxy_stat('proxy') - except Exception: - pass async def client_loop(self, addr: str) -> None: '''Loop for receiving messages from the TSUN cloud (client-side)''' diff --git a/app/src/gen3/infos_g3.py b/app/src/gen3/infos_g3.py index 04e5c69..a715d2d 100644 --- a/app/src/gen3/infos_g3.py +++ b/app/src/gen3/infos_g3.py @@ -31,6 +31,8 @@ class RegisterMap: 0xffffff06: Register.OTA_START_MSG, 0xffffff07: Register.SW_EXCEPTION, 0xffffff08: Register.MAX_DESIGNED_POWER, + 0xffffff09: Register.OUTPUT_COEFFICIENT, + 0xffffff0a: Register.INVERTER_STATUS, 0xfffffffe: Register.TEST_REG1, 0xffffffff: Register.TEST_REG2, 0x00000640: Register.OUTPUT_POWER, diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index 73ea7c0..a0c2458 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -9,12 +9,14 @@ if __name__ == "app.src.gen3.talent": from app.src.my_timer import Timer from app.src.config import Config from app.src.gen3.infos_g3 import InfosG3 + from app.src.infos import Register else: # pragma: no cover from messages import hex_dump_memory, Message, State from modbus import Modbus from my_timer import Timer from config import Config from gen3.infos_g3 import InfosG3 + from infos import Register logger = logging.getLogger('msg') @@ -78,6 +80,14 @@ class Talent(Message): ''' def close(self) -> None: logging.debug('Talent.close()') + if self.server_side: + # set inverter state to offline, if output power is very low + logging.debug('close power: ' + f'{self.db.get_db_value(Register.OUTPUT_POWER, -1)}') + if self.db.get_db_value(Register.OUTPUT_POWER, 999) < 2: + self.db.set_db_def_value(Register.INVERTER_STATUS, 0) + self.new_data['env'] = True + # we have references to methods of this class in self.switch # so we have to erase self.switch, otherwise this instance can't be # deallocated by the garbage collector ==> we get a memory leak @@ -181,7 +191,7 @@ class Talent(Message): def mb_timout_cb(self, exp_cnt): self.mb_timer.start(self.MB_REGULAR_TIMEOUT) - if 0 == (exp_cnt % 30): + if 2 == (exp_cnt % 30): # logging.info("Regular Modbus Status request") self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.DEBUG) else: diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index bf0aed8..0271ae3 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -56,8 +56,8 @@ class RegisterMap: 0x42010110: {'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 0x42010126: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501 - 0x42010170: {'reg': Register.NO_INPUTS, 'fmt': '!B'}, # noqa: E501 + 0xffffff01: {'reg': Register.OUTPUT_COEFFICIENT}, # 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf:1X}'"}, # noqa: E501 diff --git a/app/src/modbus_tcp.py b/app/src/modbus_tcp.py index 4aae57a..6674b99 100644 --- a/app/src/modbus_tcp.py +++ b/app/src/modbus_tcp.py @@ -51,7 +51,7 @@ class ModbusTcp(): while True: try: async with ModbusConn(host, port) as stream: - await stream.send_start_cmd(snr) + await stream.send_start_cmd(snr, host) await stream.loop() logger.info(f'[{stream.node_id}:{stream.conn_no}] ' f'Connection closed - Shutdown: ' diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index a127d13..24ec3b7 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -59,7 +59,7 @@ def test_default_db(): i = InfosG3P() assert json.dumps(i.db) == json.dumps({ - "inverter": {"Manufacturer": "TSUN", "Equipment_Model": "TSOL-MSxx00"}, + "inverter": {"Manufacturer": "TSUN", "Equipment_Model": "TSOL-MSxx00", "No_Inputs": 4}, "collector": {"Chip_Type": "IGEN TECH"}, }) @@ -83,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", "Version": "V4.0.10", "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}, "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}, @@ -116,8 +116,19 @@ def test_build_ha_conf1(): 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!! + assert comp == 'sensor' + assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv2_123", "val_tpl": "{{ (value_json['pv2']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV2", "sa": "Module PV2", "via_device": "inverter_123", "ids": ["input_pv2_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) + tests +=1 + elif id == 'power_pv3_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_pv3_123", "val_tpl": "{{ (value_json['pv3']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV3", "sa": "Module PV3", "via_device": "inverter_123", "ids": ["input_pv3_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) + tests +=1 + + elif id == 'power_pv4_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_pv4_123", "val_tpl": "{{ (value_json['pv4']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV4", "sa": "Module PV4", "via_device": "inverter_123", "ids": ["input_pv4_123"]}, "o": {"name": "proxy", "sw": "unknown"}}) + tests +=1 elif id == 'signal_123': assert comp == 'sensor' @@ -126,7 +137,7 @@ def test_build_ha_conf1(): elif id == 'inv_count_456': assert False - assert tests==4 + assert tests==7 for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'): @@ -138,8 +149,11 @@ def test_build_ha_conf1(): 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!! - + assert False + elif id == 'power_pv3_123': + assert False + elif id == 'power_pv4_123': + assert False elif id == 'signal_123': assert False elif id == 'inv_count_456': @@ -147,7 +161,7 @@ def test_build_ha_conf1(): 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 + assert tests==8 def test_exception_and_eval(InverterData: bytes):