From a7815bcf65510f8a51f4a6e18dcbf5d37707849e Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 16 Dec 2023 15:39:13 +0100 Subject: [PATCH 01/29] move Connect_Count into the diagnostic area --- app/src/infos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/infos.py b/app/src/infos.py index 3dd35c4..2e83354 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -121,7 +121,7 @@ class Infos: 0x000c3500: {'name': ['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': '| int', 'name': 'Signal Strength', 'icon': 'mdi:wifi'}}, # noqa: E501 0x000c96a8: {'name': ['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'name': 'Power on Time', 'val_tpl': "{{ (value_json['Power_On_Time'] | float)}}", 'nat_prc': '3', 'ent_cat': 'diagnostic'}}, # noqa: E501 0x000d0020: {'name': ['controller', 'Collect_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'data_collect_intval_', 'fmt': '| int', 'name': 'Data Collect Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501 - 0x000cfc38: {'name': ['controller', 'Connect_Count'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': '| int', 'name': 'Connect Count', 'icon': 'mdi:counter'}}, # noqa: E501 + 0x000cfc38: {'name': ['controller', 'Connect_Count'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': '| int', 'name': 'Connect Count', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 0x000c7f38: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'icon': 'mdi:wifi'}}, # noqa: E501 # 0x000c7f38: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': 's', 'new_value': 5}, # noqa: E501 0x000cf850: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'data_up_intval_', 'fmt': '| int', 'name': 'Data Up Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501 From 97079974f16d114c33cf6d75d4a3b413e1f0d4aa Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 31 Dec 2023 16:47:53 +0100 Subject: [PATCH 02/29] add schedular for regular tasks --- app/requirements.txt | 3 ++- app/src/scheduler.py | 25 +++++++++++++++++++++++++ app/src/server.py | 4 ++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 app/src/scheduler.py diff --git a/app/requirements.txt b/app/requirements.txt index 5d62110..fd06d2f 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,2 +1,3 @@ aiomqtt==1.2.1 - schema==0.7.5 \ No newline at end of file + schema==0.7.5 + aiocron==1.8 \ No newline at end of file diff --git a/app/src/scheduler.py b/app/src/scheduler.py new file mode 100644 index 0000000..f7e37be --- /dev/null +++ b/app/src/scheduler.py @@ -0,0 +1,25 @@ +import logging +from mqtt import Mqtt +from aiocron import crontab + + +class Schedule: + mqtt = None + + @classmethod + def start(cls): + logging.info("Scheduler init") + cls.mqtt = Mqtt(None) + # json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 0.0}) + # json.dumps(i.db['input']) == json.dumps({"pv1": {"Daily_Generation": 0.0}, "pv2": {"Daily_Generation": 0.0}, "pv3": {"Daily_Generation": 0.0}, "pv4": {"Daily_Generation": 0.0}}) # noqa: E501 + + crontab('0 0 * * *', func=cls.atmidnight, start=True) + + async def atmidnight(): + logging.info("Scheduler is working") + # db = self.db.db + # if key in db and self.new_data[key]: + # data_json = json.dumps(db[key]) + # node_id = self.node_id + # logger_mqtt.debug(f'{key}: {data_json}') + # await cls.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501 diff --git a/app/src/server.py b/app/src/server.py index 1bbbcd1..6ec0b85 100644 --- a/app/src/server.py +++ b/app/src/server.py @@ -6,6 +6,7 @@ import os from logging import config # noqa F401 from async_stream import AsyncStream from inverter import Inverter +from scheduler import Schedule from config import Config @@ -64,6 +65,7 @@ if __name__ == "__main__": logging.getLogger('msg').setLevel(log_level) logging.getLogger('conn').setLevel(log_level) logging.getLogger('data').setLevel(log_level) + # logging.getLogger('mqtt').setLevel(log_level) # read config file Config.read() @@ -72,6 +74,8 @@ if __name__ == "__main__": asyncio.set_event_loop(loop) Inverter.class_init() + Schedule.start() + # # Register some UNIX Signal handler for a gracefully server shutdown # on Docker restart and stop From 8fc8a29be29964aaf6de40e40e8f023828a977b4 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 6 Apr 2024 00:04:25 +0200 Subject: [PATCH 03/29] clear daily energy production at midnight --- app/proxy.svg | 379 ++++++++++++++++++------------- app/proxy.yuml | 5 + app/src/gen3/infos_g3.py | 2 +- app/src/gen3/inverter_g3.py | 2 + app/src/gen3plus/infos_g3p.py | 2 +- app/src/gen3plus/inverter_g3p.py | 2 + app/src/infos.py | 64 +++++- app/src/scheduler.py | 23 +- app/tests/test_infos.py | 24 +- 9 files changed, 327 insertions(+), 176 deletions(-) diff --git a/app/proxy.svg b/app/proxy.svg index 5d2d1d7..588835e 100644 --- a/app/proxy.svg +++ b/app/proxy.svg @@ -4,275 +4,340 @@ - - + + 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 - + +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__ + +IterRegistry + + +__iter__ 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 + +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() + +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() + +SolarmanV5 + +control +serial +snr +switch + +msg_unknown() +close() A4->A6 - - + + A7 - -ConnectionG3 - -remoteStream:ConnectionG3 - -close() + +ConnectionG3 + +remoteStream:ConnectionG3 + +close() A5->A7 - - + + A8 - -ConnectionG3P - -remoteStream:ConnectionG3P - -close() + +ConnectionG3P + +remoteStream:ConnectionG3P + +close() A6->A8 - - + + A7->A7 - - -0..1 -has + + +0..1 +has A11 - -InverterG3 - -__ha_restarts - -async_create_remote() -close() + +InverterG3 + +__ha_restarts + +async_create_remote() +close() A7->A11 - - + + A8->A8 - - -0..1 -has + + +0..1 +has A12 - -InverterG3P - -__ha_restarts - -async_create_remote() -close() + +InverterG3P + +__ha_restarts + +async_create_remote() +close() A8->A12 - - + + 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() + +AsyncStream + +reader +writer +addr +r_addr +l_addr + +<async>server_loop() +<async>client_loop() +<async>loop +disc() +close() +__async_read() +__async_write() +__async_forward() A9->A7 - - + + A9->A8 - - + + A10->A11 - - + + A10->A12 - - + + + + + +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 + + + +A14 + +InfosG3 + + +ha_confs() +parse() + + + +A13->A14 + + + + + +A15 + +InfosG3P + + +ha_confs() +parse() + + + +A13->A15 + + + + + +A14->A5 + + + + + +A15->A6 + + diff --git a/app/proxy.yuml b/app/proxy.yuml index a7c47c9..7f5be21 100644 --- a/app/proxy.yuml +++ b/app/proxy.yuml @@ -19,3 +19,8 @@ [ConnectionG3]has-0..1>[ConnectionG3] [ConnectionG3P]^[InverterG3P] [ConnectionG3P]has-0..1>[ConnectionG3P] +[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]^[InfosG3||ha_confs();parse()] +[Infos]^[InfosG3P||ha_confs();parse()] +[InfosG3P]->[SolarmanV5] +[InfosG3]->[Talent] + diff --git a/app/src/gen3/infos_g3.py b/app/src/gen3/infos_g3.py index 726f289..7e45634 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'{name} : {result}{unit}' + self.tracer.log(level, f'GEN3: {name} : {result}{unit}' f' update: {update}') i += 1 diff --git a/app/src/gen3/inverter_g3.py b/app/src/gen3/inverter_g3.py index e9db0f4..f9a737e 100644 --- a/app/src/gen3/inverter_g3.py +++ b/app/src/gen3/inverter_g3.py @@ -116,6 +116,8 @@ class InverterG3(Inverter, ConnectionG3): await self.mqtt.publish(f"{self.discovery_prfx}{component}" f"/{node_id}{id}/config", data_json) + self.db.reg_clr_at_midnight(f'{self.entity_prfx}{node_id}') + def close(self) -> None: logging.debug(f'InverterG3.close() l{self.l_addr} | r{self.r_addr}') super().close() # call close handler in the parent class diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index 355600c..c92bdb2 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -117,5 +117,5 @@ class InfosG3P(Infos): name = str(f'info-id.0x{addr:x}') update = False - self.tracer.log(level, f'{name} : {result}{unit}' + self.tracer.log(level, f'GEN3PLUS: {name} : {result}{unit}' f' update: {update}') diff --git a/app/src/gen3plus/inverter_g3p.py b/app/src/gen3plus/inverter_g3p.py index 70e8d5c..75f69af 100644 --- a/app/src/gen3plus/inverter_g3p.py +++ b/app/src/gen3plus/inverter_g3p.py @@ -116,6 +116,8 @@ class InverterG3P(Inverter, ConnectionG3P): await self.mqtt.publish(f"{self.discovery_prfx}{component}" f"/{node_id}{id}/config", data_json) + self.db.reg_clr_at_midnight(f'{self.entity_prfx}{node_id}') + def close(self) -> None: logging.debug(f'InverterG3P.close() l{self.l_addr} | r{self.r_addr}') super().close() # call close handler in the parent class diff --git a/app/src/infos.py b/app/src/infos.py index 51be275..6262c65 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -95,6 +95,34 @@ class Register(Enum): TEST_REG2 = 10001 +class ClrAtMidnight: + __clr_at_midnight = [Register.PV1_DAILY_GENERATION, Register.PV2_DAILY_GENERATION, Register.PV3_DAILY_GENERATION, Register.PV4_DAILY_GENERATION, Register.PV5_DAILY_GENERATION, Register.PV6_DAILY_GENERATION, Register.DAILY_GENERATION] # noqa: E501 + db = {} + + @classmethod + def add(cls, keys, prfx: str, reg: Register): + if reg not in cls.__clr_at_midnight: + return + + prfx += f'{keys[0]}' + dict = cls.db + if prfx not in dict: + dict[prfx] = {} + dict = dict[prfx] + + for key in keys[1:-1]: + if key not in dict: # pragma: no cover + dict[key] = {} + dict = dict[key] + dict[keys[-1]] = 0 + + @classmethod + def elm(cls) -> Generator: + for reg, name in cls.db.items(): + yield reg, name + cls.db = {} + + class Infos: stat = {} app_name = os.getenv('SERVICE_NAME', 'proxy') @@ -129,6 +157,8 @@ class Infos: 'input_pv2': {'via': 'inverter', 'name': 'Module PV2', 'dep': {'reg': Register.NO_INPUTS, 'gte': 2}}, # noqa: E501 'input_pv3': {'via': 'inverter', 'name': 'Module PV3', 'dep': {'reg': Register.NO_INPUTS, 'gte': 3}}, # noqa: E501 'input_pv4': {'via': 'inverter', 'name': 'Module PV4', 'dep': {'reg': Register.NO_INPUTS, 'gte': 4}}, # noqa: E501 + 'input_pv5': {'via': 'inverter', 'name': 'Module PV5', 'dep': {'reg': Register.NO_INPUTS, 'gte': 5}}, # noqa: E501 + 'input_pv6': {'via': 'inverter', 'name': 'Module PV6', 'dep': {'reg': Register.NO_INPUTS, 'gte': 6}}, # noqa: E501 } __comm_type_val_tpl = "{%set com_types = ['n/a','Wi-Fi', 'G4', 'G5', 'GPRS'] %}{{com_types[value_json['Communication_Type']|int(0)]|default(value_json['Communication_Type'])}}" # noqa: E501 @@ -193,16 +223,22 @@ class Infos: # input measures: Register.PV1_VOLTAGE: {'name': ['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv1_', 'val_tpl': "{{ (value_json['pv1']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV1_CURRENT: {'name': ['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv1_', 'val_tpl': "{{ (value_json['pv1']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 - Register.PV1_POWER: {'name': ['input', 'pv1', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv1_', 'val_tpl': "{{ (value_json['pv1']['Power'] | float)}}"}}, # noqa: E501 + Register.PV1_POWER: {'name': ['input', 'pv1', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv1_', 'val_tpl': "{{ (value_json['pv1']['Power'] | float)}}"}}, # noqa: E501 Register.PV2_VOLTAGE: {'name': ['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv2_', 'val_tpl': "{{ (value_json['pv2']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV2_CURRENT: {'name': ['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv2_', 'val_tpl': "{{ (value_json['pv2']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 - Register.PV2_POWER: {'name': ['input', 'pv2', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv2_', 'val_tpl': "{{ (value_json['pv2']['Power'] | float)}}"}}, # noqa: E501 + Register.PV2_POWER: {'name': ['input', 'pv2', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv2_', 'val_tpl': "{{ (value_json['pv2']['Power'] | float)}}"}}, # noqa: E501 Register.PV3_VOLTAGE: {'name': ['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv3_', 'val_tpl': "{{ (value_json['pv3']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV3_CURRENT: {'name': ['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv3_', 'val_tpl': "{{ (value_json['pv3']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 - Register.PV3_POWER: {'name': ['input', 'pv3', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv3_', 'val_tpl': "{{ (value_json['pv3']['Power'] | float)}}"}}, # noqa: E501 + Register.PV3_POWER: {'name': ['input', 'pv3', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv3_', 'val_tpl': "{{ (value_json['pv3']['Power'] | float)}}"}}, # noqa: E501 Register.PV4_VOLTAGE: {'name': ['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv4_', 'val_tpl': "{{ (value_json['pv4']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV4_CURRENT: {'name': ['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv4_', 'val_tpl': "{{ (value_json['pv4']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 - Register.PV4_POWER: {'name': ['input', 'pv4', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv4_', 'val_tpl': "{{ (value_json['pv4']['Power'] | float)}}"}}, # noqa: E501 + Register.PV4_POWER: {'name': ['input', 'pv4', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv4_', 'val_tpl': "{{ (value_json['pv4']['Power'] | float)}}"}}, # noqa: E501 + Register.PV5_VOLTAGE: {'name': ['input', 'pv5', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv5', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv5_', 'val_tpl': "{{ (value_json['pv5']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.PV5_CURRENT: {'name': ['input', 'pv5', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv5', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv5_', 'val_tpl': "{{ (value_json['pv5']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.PV5_POWER: {'name': ['input', 'pv5', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv5', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv5_', 'val_tpl': "{{ (value_json['pv5']['Power'] | float)}}"}}, # noqa: E501 + Register.PV6_VOLTAGE: {'name': ['input', 'pv6', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv6', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv6_', 'val_tpl': "{{ (value_json['pv6']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.PV6_CURRENT: {'name': ['input', 'pv6', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv6', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv6_', 'val_tpl': "{{ (value_json['pv6']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 + Register.PV6_POWER: {'name': ['input', 'pv6', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'input_pv6', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv6_', 'val_tpl': "{{ (value_json['pv6']['Power'] | float)}}"}}, # noqa: E501 Register.PV1_DAILY_GENERATION: {'name': ['input', 'pv1', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv1_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv1']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 Register.PV1_TOTAL_GENERATION: {'name': ['input', 'pv1', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv1_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv1']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 Register.PV2_DAILY_GENERATION: {'name': ['input', 'pv2', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv2_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv2']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 @@ -211,6 +247,10 @@ class Infos: Register.PV3_TOTAL_GENERATION: {'name': ['input', 'pv3', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv3_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv3']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 Register.PV4_DAILY_GENERATION: {'name': ['input', 'pv4', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv4_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv4']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 Register.PV4_TOTAL_GENERATION: {'name': ['input', 'pv4', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv4_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv4']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 + Register.PV5_DAILY_GENERATION: {'name': ['input', 'pv5', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv5', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv5_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv5']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 + Register.PV5_TOTAL_GENERATION: {'name': ['input', 'pv5', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv5', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv5_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv5']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 + Register.PV6_DAILY_GENERATION: {'name': ['input', 'pv6', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv6', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv6_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv6']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 + Register.PV6_TOTAL_GENERATION: {'name': ['input', 'pv6', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv6', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv6_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv6']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 # total: Register.DAILY_GENERATION: {'name': ['total', 'Daily_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_', 'fmt': '| float', 'name': 'Daily Generation', 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 Register.TOTAL_GENERATION: {'name': ['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': '| float', 'name': 'Total Generation', 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 @@ -437,14 +477,26 @@ class Infos: def set_db_def_value(self, id, value): '''set default value''' row = self.info_defs[id] - if isinstance(row, dict): # pragma: no cover + if isinstance(row, dict): keys = row['name'] self.update_db(keys, False, value) + def reg_clr_at_midnight(self, prfx: str): + for id, row in self.info_defs.items(): + if 'ha' in row: + ha = row['ha'] + if 'dev' in ha: + device = self.info_devs[ha['dev']] + if 'dep' in device and self.ignore_this_device(device['dep']): # noqa: E501 + continue + + keys = row['name'] + ClrAtMidnight.add(keys, prfx, id) + def get_db_value(self, id, not_found_result=None): '''get database value''' row = self.info_defs[id] - if isinstance(row, dict): # pragma: no cover + if isinstance(row, dict): keys = row['name'] elm = self.db for key in keys[:-1]: diff --git a/app/src/scheduler.py b/app/src/scheduler.py index f7e37be..37acb80 100644 --- a/app/src/scheduler.py +++ b/app/src/scheduler.py @@ -1,6 +1,10 @@ import logging +import json from mqtt import Mqtt from aiocron import crontab +from infos import ClrAtMidnight + +logger_mqtt = logging.getLogger('mqtt') class Schedule: @@ -10,16 +14,15 @@ class Schedule: def start(cls): logging.info("Scheduler init") cls.mqtt = Mqtt(None) - # json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 0.0}) - # json.dumps(i.db['input']) == json.dumps({"pv1": {"Daily_Generation": 0.0}, "pv2": {"Daily_Generation": 0.0}, "pv3": {"Daily_Generation": 0.0}, "pv4": {"Daily_Generation": 0.0}}) # noqa: E501 crontab('0 0 * * *', func=cls.atmidnight, start=True) + # crontab('*/5 * * * *', func=cls.atmidnight, start=True) - async def atmidnight(): - logging.info("Scheduler is working") - # db = self.db.db - # if key in db and self.new_data[key]: - # data_json = json.dumps(db[key]) - # node_id = self.node_id - # logger_mqtt.debug(f'{key}: {data_json}') - # await cls.mqtt.publish(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501 + @classmethod + async def atmidnight(cls): + logging.info("Clear daily counters at midnight") + + for key, data in ClrAtMidnight.elm(): + logger_mqtt.debug(f'{key}: {data}') + data_json = json.dumps(data) + await cls.mqtt.publish(f"{key}", data_json) diff --git a/app/tests/test_infos.py b/app/tests/test_infos.py index a018af0..f799456 100644 --- a/app/tests/test_infos.py +++ b/app/tests/test_infos.py @@ -1,6 +1,6 @@ # test_with_pytest.py import pytest, json -from app.src.infos import Register +from app.src.infos import Register, ClrAtMidnight from app.src.gen3.infos_g3 import InfosG3 @pytest.fixture @@ -524,3 +524,25 @@ def test_invalid_data_type(InvalidDataSeq): val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter assert val == 1 +def test_clr_at_midnight(): + i = InfosG3() + i.static_init() # initialize counter + i.set_db_def_value(Register.NO_INPUTS, 2) + val = i.dev_value(Register.NO_INPUTS) # valid addr but not initiliazed + assert val == 2 + + i.reg_clr_at_midnight('tsun/inv_1/') + # tsun/inv_2/input + assert json.dumps(ClrAtMidnight.db['tsun/inv_1/total']) == json.dumps({'Daily_Generation': 0}) + assert json.dumps(ClrAtMidnight.db['tsun/inv_1/input']) == json.dumps({"pv1": {"Daily_Generation": 0}, "pv2": {"Daily_Generation": 0}}) + + test = 0 + for key, data in ClrAtMidnight.elm(): + if key == 'tsun/inv_1/total': + assert json.dumps(data) == json.dumps({'Daily_Generation': 0}) + test += 1 + elif key == 'tsun/inv_1/input': + assert json.dumps(data) == json.dumps({"pv1": {"Daily_Generation": 0}, "pv2": {"Daily_Generation": 0}}) + test += 1 + assert test == 2 + assert json.dumps(ClrAtMidnight.db) == json.dumps({}) From 156eb06b6ac5c76d37ad8a593dfedf884d8dd78b Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 6 Apr 2024 20:13:53 +0200 Subject: [PATCH 04/29] add changes --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8ebeb6..d50288f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Prepare support of inverters with 6 MTPPs +- Clear `Daily Generation` values at midnigth + ## [0.6.0] - 2024-04-02 - Refactoring to support Solarman V5 protocol From 2153d7c15c96c03440ef61e5169391635799d519 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 6 Apr 2024 20:20:42 +0200 Subject: [PATCH 05/29] cleanup --- app/src/infos.py | 45 ++++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/app/src/infos.py b/app/src/infos.py index 6262c65..afcc4a7 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -100,7 +100,7 @@ class ClrAtMidnight: db = {} @classmethod - def add(cls, keys, prfx: str, reg: Register): + def add(cls, keys: list, prfx: str, reg: Register) -> None: if reg not in cls.__clr_at_midnight: return @@ -117,7 +117,7 @@ class ClrAtMidnight: dict[keys[-1]] = 0 @classmethod - def elm(cls) -> Generator: + def elm(cls) -> Generator[tuple[str, dict], None, None]: for reg, name in cls.db.items(): yield reg, name cls.db = {} @@ -273,24 +273,14 @@ class Infos: @property def info_defs(self) -> dict: return self.__info_defs - ''' - if __name__ == "app.src.messages": - @info_defs.setter - def info_defs(self, value: dict) -> None: - self.__info_defs = value - - @info_devs.setter - def info_devs(self, value: dict) -> None: - self.__info_devs = value - ''' def dev_value(self, idx: str | int) -> str | int | float | None: '''returns the stored device value from our database idx:int ==> lookup the value in the database and return it as str, - int or flout. If the value is not available return 'None' + int or float. If the value is not available return 'None' idx:str ==> returns the string as a fixed value without a - database loopup + database lookup ''' if type(idx) is str: return idx # return idx as a fixed value @@ -340,7 +330,8 @@ class Infos: if res: yield res - def ha_conf(self, key, ha_prfx, node_id, snr, singleton: bool, sug_area: str = '') -> tuple[str, str, str, str]: # noqa: E501 + def ha_conf(self, key, ha_prfx, node_id, snr, singleton: bool, + sug_area: str = '') -> tuple[str, str, str, str] | None: if key not in self.info_defs: return None row = self.info_defs[key] @@ -444,7 +435,7 @@ class Infos: return json.dumps(attr), component, node_id, attr['uniq_id'] return None - def _key_obj(self, id) -> list: + def _key_obj(self, id: Register) -> list: d = self.info_defs.get(id, {'name': None, 'level': logging.DEBUG, 'unit': ''}) if 'ha' in d and 'must_incr' in d['ha']: @@ -454,7 +445,7 @@ class Infos: return d['name'], d['level'], d['unit'], must_incr - def update_db(self, keys, must_incr, result): + def update_db(self, keys: list, must_incr: bool, result): name = '' dict = self.db for key in keys[:-1]: @@ -474,14 +465,20 @@ class Infos: name += keys[-1] return name, update - def set_db_def_value(self, id, value): + def set_db_def_value(self, id: Register, value) -> None: '''set default value''' row = self.info_defs[id] if isinstance(row, dict): keys = row['name'] self.update_db(keys, False, value) - def reg_clr_at_midnight(self, prfx: str): + def reg_clr_at_midnight(self, prfx: str) -> None: + '''register all registers for the 'ClrAtMidnight' class and + check if device of every register is available otherwise ignore + the register. + + prfx:str ==> prefix for the home assistant 'stat_t string'' + ''' for id, row in self.info_defs.items(): if 'ha' in row: ha = row['ha'] @@ -493,23 +490,21 @@ class Infos: keys = row['name'] ClrAtMidnight.add(keys, prfx, id) - def get_db_value(self, id, not_found_result=None): + def get_db_value(self, id: Register, not_found_result: any = None): '''get database value''' row = self.info_defs[id] if isinstance(row, dict): keys = row['name'] elm = self.db - for key in keys[:-1]: + for key in keys: if key not in elm: return not_found_result elm = elm[key] - - if keys[-1] in elm: - return elm[keys[-1]] + return elm return not_found_result def ignore_this_device(self, dep: dict) -> bool: - '''Checks the equation in the dep dict + '''Checks the equation in the dep(endency) dict returns 'False' only if the equation is valid; 'True' in any other case''' From eadd85a12537a49ba37224a7ea4be0cef8b7d5f5 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 6 Apr 2024 20:45:54 +0200 Subject: [PATCH 06/29] add dev-* branches for push trigger --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index af96376..8f4f09a 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -5,7 +5,7 @@ name: Python application on: push: - branches: [ "main" ] + branches: [ "main", "dev-*" ] paths-ignore: - '**.md' # Do no build on *.md changes - '**.yml' # Do no build on *.yml changes From b9731d43a61563da5137e3049335268e7c789b5c Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 6 Apr 2024 21:08:09 +0200 Subject: [PATCH 07/29] add docstrings to the scheduler module --- app/src/scheduler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/scheduler.py b/app/src/scheduler.py index 37acb80..5c4eb2d 100644 --- a/app/src/scheduler.py +++ b/app/src/scheduler.py @@ -11,7 +11,8 @@ class Schedule: mqtt = None @classmethod - def start(cls): + def start(cls) -> None: + '''Start the scheduler and schedule the tasks (cron jobs)''' logging.info("Scheduler init") cls.mqtt = Mqtt(None) @@ -19,7 +20,8 @@ class Schedule: # crontab('*/5 * * * *', func=cls.atmidnight, start=True) @classmethod - async def atmidnight(cls): + async def atmidnight(cls) -> None: + '''Clear daily counters at midnight''' logging.info("Clear daily counters at midnight") for key, data in ClrAtMidnight.elm(): From 214f3dfae593f67bda1df78c3efed3601cdd57f3 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 7 Apr 2024 10:29:05 +0200 Subject: [PATCH 08/29] Add manufacturuer and modell type for pv modules --- app/src/infos.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/app/src/infos.py b/app/src/infos.py index afcc4a7..21c121e 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -34,21 +34,33 @@ class Register(Enum): PV1_VOLTAGE = 100 PV1_CURRENT = 101 PV1_POWER = 102 + PV1_MANUFACTURER = 103 + PV1_MODEL = 104 PV2_VOLTAGE = 110 PV2_CURRENT = 111 PV2_POWER = 112 + PV2_MANUFACTURER = 113 + PV2_MODEL = 114 PV3_VOLTAGE = 120 PV3_CURRENT = 121 PV3_POWER = 122 + PV3_MANUFACTURER = 123 + PV3_MODEL = 124 PV4_VOLTAGE = 130 PV4_CURRENT = 131 PV4_POWER = 132 + PV4_MANUFACTURER = 133 + PV4_MODEL = 134 PV5_VOLTAGE = 140 PV5_CURRENT = 141 PV5_POWER = 142 + PV5_MANUFACTURER = 143 + PV5_MODEL = 144 PV6_VOLTAGE = 150 PV6_CURRENT = 151 PV6_POWER = 152 + PV6_MANUFACTURER = 153 + PV6_MODEL = 154 PV1_DAILY_GENERATION = 200 PV1_TOTAL_GENERATION = 201 PV2_DAILY_GENERATION = 210 @@ -153,12 +165,12 @@ class Infos: 'proxy': {'singleton': True, 'name': 'Proxy', 'mf': 'Stefan Allius'}, # noqa: E501 'controller': {'via': 'proxy', 'name': 'Controller', 'mdl': Register.CHIP_MODEL, 'mf': Register.CHIP_TYPE, 'sw': Register.COLLECTOR_FW_VERSION}, # noqa: E501 'inverter': {'via': 'controller', 'name': 'Micro Inverter', 'mdl': Register.EQUIPMENT_MODEL, 'mf': Register.MANUFACTURER, 'sw': Register.VERSION}, # noqa: E501 - 'input_pv1': {'via': 'inverter', 'name': 'Module PV1'}, - 'input_pv2': {'via': 'inverter', 'name': 'Module PV2', 'dep': {'reg': Register.NO_INPUTS, 'gte': 2}}, # noqa: E501 - 'input_pv3': {'via': 'inverter', 'name': 'Module PV3', 'dep': {'reg': Register.NO_INPUTS, 'gte': 3}}, # noqa: E501 - 'input_pv4': {'via': 'inverter', 'name': 'Module PV4', 'dep': {'reg': Register.NO_INPUTS, 'gte': 4}}, # noqa: E501 - 'input_pv5': {'via': 'inverter', 'name': 'Module PV5', 'dep': {'reg': Register.NO_INPUTS, 'gte': 5}}, # noqa: E501 - 'input_pv6': {'via': 'inverter', 'name': 'Module PV6', 'dep': {'reg': Register.NO_INPUTS, 'gte': 6}}, # noqa: E501 + 'input_pv1': {'via': 'inverter', 'name': 'Module PV1', 'mdl': Register.PV1_MODEL, 'mf': Register.PV1_MANUFACTURER}, # noqa: E501 + 'input_pv2': {'via': 'inverter', 'name': 'Module PV2', 'mdl': Register.PV2_MODEL, 'mf': Register.PV2_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 2}}, # noqa: E501 + 'input_pv3': {'via': 'inverter', 'name': 'Module PV3', 'mdl': Register.PV3_MODEL, 'mf': Register.PV3_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 3}}, # noqa: E501 + 'input_pv4': {'via': 'inverter', 'name': 'Module PV4', 'mdl': Register.PV4_MODEL, 'mf': Register.PV4_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 4}}, # noqa: E501 + 'input_pv5': {'via': 'inverter', 'name': 'Module PV5', 'mdl': Register.PV5_MODEL, 'mf': Register.PV5_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 5}}, # noqa: E501 + 'input_pv6': {'via': 'inverter', 'name': 'Module PV6', 'mdl': Register.PV6_MODEL, 'mf': Register.PV6_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 6}}, # noqa: E501 } __comm_type_val_tpl = "{%set com_types = ['n/a','Wi-Fi', 'G4', 'G5', 'GPRS'] %}{{com_types[value_json['Communication_Type']|int(0)]|default(value_json['Communication_Type'])}}" # noqa: E501 From 9159882f854a26b049f00a8456a9585c38b37fcd Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 7 Apr 2024 10:33:14 +0200 Subject: [PATCH 09/29] Add iocron badge to README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 538f7cf..cae371d 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ License: BSD-3-Clause Supported Python versions Supported aiomqtt versions + Supported aiocron versions Supported toml versions

From 4d6813ae7c8d4ea8a7fcbb8bdab341159503c17c Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 7 Apr 2024 10:57:17 +0200 Subject: [PATCH 10/29] - fix TSUN model names --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cae371d..1225ecb 100644 --- a/README.md +++ b/README.md @@ -40,12 +40,12 @@ If you use a Pi-hole, you can also store the host entry in the Pi-hole. ## Features -- supports TSUN GEN3 inverters: TSOL MS-300, MS-350, MS-400, MS-600, MS-700 and MS-800 -- support for TSUN GEN3 PLUS inverters since proxy version 0.6 (e.g. MS-2000) +- 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 - Self-sufficient island operation without internet (for TSUN GEN3 PLUS inverters in preparation) -- non-root Docker Container +- runs in a non-root Docker Container ## Home Assistant Screenshots From 93b89062f5bafbbf5256184a2f72844e0858e45a Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 7 Apr 2024 19:41:05 +0200 Subject: [PATCH 11/29] Read pv module details for HA from config file --- CHANGELOG.md | 2 ++ app/config/default_config.toml | 10 +++++++++- app/src/config.py | 26 +++++++++++++++++++++++++- app/src/gen3/talent.py | 5 +++-- app/src/gen3plus/solarman_v5.py | 9 +++++---- app/src/infos.py | 30 ++++++++++++++++++++++++++++++ 6 files changed, 74 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d50288f..86483ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prepare support of inverters with 6 MTPPs - Clear `Daily Generation` values at midnigth +- Read pv module details from config file and use it for the Home Assistant registration + see: [#43](https://github.com/s-allius/tsun-gen3-proxy/issues/43) ## [0.6.0] - 2024-04-02 diff --git a/app/config/default_config.toml b/app/config/default_config.toml index fccc54b..050ca32 100644 --- a/app/config/default_config.toml +++ b/app/config/default_config.toml @@ -31,13 +31,21 @@ inverters.allow_all = true # allow inverters, even if we have no inverter mapp [inverters."R170000000000001"] #node_id = '' # Optional, MQTT replacement for inverters serial number #suggested_area = '' # Optional, suggested installation area for home-assistant +#pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} +#pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} #[inverters."R17xxxxxxxxxxxx2"] #node_id = '' # Optional, MQTT replacement for inverters serial number #suggested_area = '' # Optional, suggested installation area for home-assistant +#pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} +#pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} [inverters."Y170000000000001"] -#monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter +monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter #node_id = '' # Optional, MQTT replacement for inverters serial number #suggested_area = '' # Optional, suggested installation place for home-assistant +#pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} +#pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} +#pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} +#pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} diff --git a/app/src/config.py b/app/src/config.py index 3778e09..589bb6c 100644 --- a/app/src/config.py +++ b/app/src/config.py @@ -45,7 +45,31 @@ class Config(): if len(s) > 0 and s[-1] != '/' else s)), - Optional('suggested_area', default=""): Use(str) + Optional('suggested_area', default=""): Use(str), + Optional('pv1'): { + Optional('type'): Use(str), + Optional('manufacturer'): Use(str), + }, + Optional('pv2'): { + Optional('type'): Use(str), + Optional('manufacturer'): Use(str), + }, + Optional('pv3'): { + Optional('type'): Use(str), + Optional('manufacturer'): Use(str), + }, + Optional('pv4'): { + Optional('type'): Use(str), + Optional('manufacturer'): Use(str), + }, + Optional('pv5'): { + Optional('type'): Use(str), + Optional('manufacturer'): Use(str), + }, + Optional('pv6'): { + Optional('type'): Use(str), + Optional('manufacturer'): Use(str), + } }} }, ignore_extra_keys=True ) diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index 0bc08a8..46302ac 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -59,7 +59,7 @@ class Talent(Message): # deallocated by the garbage collector ==> we get a memory leak self.switch.clear() - def set_serial_no(self, serial_no: str): + def __set_serial_no(self, serial_no: str): if self.unique_id == serial_no: logger.debug(f'SerialNo: {serial_no}') @@ -72,6 +72,7 @@ class Talent(Message): self.node_id = inv['node_id'] self.sug_area = inv['suggested_area'] logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501 + self.db.set_pv_module_details(inv) else: self.node_id = '' self.sug_area = '' @@ -95,7 +96,7 @@ class Talent(Message): hex_dump_memory(logging.INFO, f'Received from {self.addr}:', self._recv_buffer, self.header_len+self.data_len) - self.set_serial_no(self.id_str.decode("utf-8")) + self.__set_serial_no(self.id_str.decode("utf-8")) self.__dispatch_msg() self.__flush_recv_msg() return diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index c09512b..fc189b3 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -70,7 +70,7 @@ class SolarmanV5(Message): # deallocated by the garbage collector ==> we get a memory leak self.switch.clear() - def set_serial_no(self, snr: int): + def __set_serial_no(self, snr: int): serial_no = str(snr) if self.unique_id == serial_no: logger.debug(f'SerialNo: {serial_no}') @@ -87,6 +87,7 @@ class SolarmanV5(Message): self.node_id = inv['node_id'] self.sug_area = inv['suggested_area'] logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501 + self.db.set_pv_module_details(inv) if not found: self.node_id = '' @@ -112,7 +113,7 @@ class SolarmanV5(Message): self._recv_buffer, self.header_len+self.data_len+2) if self.__trailer_is_ok(self._recv_buffer, self.header_len + self.data_len + 2): - self.set_serial_no(self.snr) + self.__set_serial_no(self.snr) self.__dispatch_msg() self.__flush_recv_msg() return @@ -352,9 +353,9 @@ class SolarmanV5(Message): ftype = result[0] # always 2 valid = result[1] == 1 # status ts = result[2] - repeat = result[3] # always 60 + set_hb = result[3] # always 60 or 120 logger.info(f'ftype:{ftype} accepted:{valid}' - f' ts:{ts:08x} repeat:{repeat}s') + f' ts:{ts:08x} nextHeartbeat: {set_hb}s') dt = datetime.fromtimestamp(ts) logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}') diff --git a/app/src/infos.py b/app/src/infos.py index 21c121e..88e70c4 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -193,6 +193,19 @@ class Infos: 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.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 + Register.PV1_MODEL: {'name': ['inverter', 'PV1_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + Register.PV2_MANUFACTURER: {'name': ['inverter', 'PV2_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + Register.PV2_MODEL: {'name': ['inverter', 'PV2_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + Register.PV3_MANUFACTURER: {'name': ['inverter', 'PV3_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + Register.PV3_MODEL: {'name': ['inverter', 'PV3_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + Register.PV4_MANUFACTURER: {'name': ['inverter', 'PV4_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + Register.PV4_MODEL: {'name': ['inverter', 'PV4_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + Register.PV5_MANUFACTURER: {'name': ['inverter', 'PV5_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + Register.PV5_MODEL: {'name': ['inverter', 'PV5_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + Register.PV6_MANUFACTURER: {'name': ['inverter', 'PV6_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + Register.PV6_MODEL: {'name': ['inverter', 'PV6_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + # proxy: Register.INVERTER_CNT: {'name': ['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_count_', 'fmt': '| int', 'name': 'Active Inverter Connections', 'icon': 'mdi:counter'}}, # noqa: E501 Register.UNKNOWN_SNR: {'name': ['proxy', 'Unknown_SNR'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_snr_', 'fmt': '| int', 'name': 'Unknown Serial No', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 @@ -530,3 +543,20 @@ class Infos: elif 'less_eq' in dep: return not value <= dep['less_eq'] return True + + def set_pv_module_details(self, inv: dict) -> None: + map = {'pv1': {'manufacturer': Register.PV1_MANUFACTURER, 'model': Register.PV1_MODEL}, # noqa: E501 + 'pv2': {'manufacturer': Register.PV2_MANUFACTURER, 'model': Register.PV2_MODEL}, # noqa: E501 + 'pv3': {'manufacturer': Register.PV3_MANUFACTURER, 'model': Register.PV3_MODEL}, # noqa: E501 + 'pv4': {'manufacturer': Register.PV4_MANUFACTURER, 'model': Register.PV4_MODEL}, # noqa: E501 + 'pv5': {'manufacturer': Register.PV5_MANUFACTURER, 'model': Register.PV5_MODEL}, # noqa: E501 + 'pv6': {'manufacturer': Register.PV6_MANUFACTURER, 'model': Register.PV6_MODEL} # noqa: E501 + } + + for key, reg in map.items(): + if key in inv: + if 'manufacturer' in inv[key]: + self.set_db_def_value(reg['manufacturer'], + inv[key]['manufacturer']) + if 'type' in inv[key]: + self.set_db_def_value(reg['model'], inv[key]['type']) From 35bbfee80ae4ac8a54301b79b819c5b787c09cf3 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 7 Apr 2024 20:02:39 +0200 Subject: [PATCH 12/29] fix name of aiocron badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1225ecb..f35de3c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ License: BSD-3-Clause Supported Python versions Supported aiomqtt versions - Supported aiocron versions + Supported aiocron versions Supported toml versions

From 9d395af9861897aa184dbd55bef34908c5d92db0 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 7 Apr 2024 20:52:07 +0200 Subject: [PATCH 13/29] add samples for pv module configurations --- app/config/default_config.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/config/default_config.toml b/app/config/default_config.toml index 050ca32..cd95d75 100644 --- a/app/config/default_config.toml +++ b/app/config/default_config.toml @@ -31,21 +31,21 @@ inverters.allow_all = true # allow inverters, even if we have no inverter mapp [inverters."R170000000000001"] #node_id = '' # Optional, MQTT replacement for inverters serial number #suggested_area = '' # Optional, suggested installation area for home-assistant -#pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} -#pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} +#pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr +#pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr #[inverters."R17xxxxxxxxxxxx2"] #node_id = '' # Optional, MQTT replacement for inverters serial number #suggested_area = '' # Optional, suggested installation area for home-assistant -#pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} -#pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} +#pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr +#pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr [inverters."Y170000000000001"] monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter #node_id = '' # Optional, MQTT replacement for inverters serial number #suggested_area = '' # Optional, suggested installation place for home-assistant -#pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} -#pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} -#pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} -#pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} +#pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr +#pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr +#pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr +#pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr From 06b896d6e969af9b12766441574afdec05e7b7fb Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 7 Apr 2024 20:52:48 +0200 Subject: [PATCH 14/29] add samples for pv module configurations --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 538f7cf..3487ce8 100644 --- a/README.md +++ b/README.md @@ -127,15 +127,23 @@ inverters.allow_all = false # True: allow inverters, even if we have no invert [inverters."R17xxxxxxxxxxxx1"] node_id = 'inv1' # Optional, MQTT replacement for inverters serial number suggested_area = 'roof' # Optional, suggested installation area for home-assistant +pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr +pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr [inverters."R17xxxxxxxxxxxx2"] node_id = 'inv2' # Optional, MQTT replacement for inverters serial number suggested_area = 'balcony' # Optional, suggested installation area for home-assistant +pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr +pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr [inverters."Y17xxxxxxxxxxxx1"] monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter node_id = 'inv_3' # MQTT replacement for inverters serial number suggested_area = 'garage' # suggested installation place for home-assistant +pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr +pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr +pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr +pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr ``` From 97da24c8397e13878e464f2a57763209cd199890 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sun, 7 Apr 2024 22:44:53 +0200 Subject: [PATCH 15/29] add missing tests --- app/src/infos.py | 2 +- app/tests/test_infos.py | 46 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/app/src/infos.py b/app/src/infos.py index 88e70c4..5fa0697 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -123,7 +123,7 @@ class ClrAtMidnight: dict = dict[prfx] for key in keys[1:-1]: - if key not in dict: # pragma: no cover + if key not in dict: dict[key] = {} dict = dict[key] dict[keys[-1]] = 0 diff --git a/app/tests/test_infos.py b/app/tests/test_infos.py index f799456..8bf419d 100644 --- a/app/tests/test_infos.py +++ b/app/tests/test_infos.py @@ -530,12 +530,18 @@ def test_clr_at_midnight(): i.set_db_def_value(Register.NO_INPUTS, 2) val = i.dev_value(Register.NO_INPUTS) # valid addr but not initiliazed assert val == 2 - + i.info_defs[Register.TEST_REG1] = { # add a entry with incomplete ha definition + 'name': ['test', 'grp', 'REG_1'], 'ha': {'dev_cla': None } + } i.reg_clr_at_midnight('tsun/inv_1/') # tsun/inv_2/input assert json.dumps(ClrAtMidnight.db['tsun/inv_1/total']) == json.dumps({'Daily_Generation': 0}) assert json.dumps(ClrAtMidnight.db['tsun/inv_1/input']) == json.dumps({"pv1": {"Daily_Generation": 0}, "pv2": {"Daily_Generation": 0}}) + i.reg_clr_at_midnight('tsun/inv_1/') + assert json.dumps(ClrAtMidnight.db['tsun/inv_1/total']) == json.dumps({'Daily_Generation': 0}) + assert json.dumps(ClrAtMidnight.db['tsun/inv_1/input']) == json.dumps({"pv1": {"Daily_Generation": 0}, "pv2": {"Daily_Generation": 0}}) + test = 0 for key, data in ClrAtMidnight.elm(): if key == 'tsun/inv_1/total': @@ -546,3 +552,41 @@ def test_clr_at_midnight(): test += 1 assert test == 2 assert json.dumps(ClrAtMidnight.db) == json.dumps({}) + + i.reg_clr_at_midnight('tsun/inv_1/') + + + + +def test_pv_module_config(): + i = InfosG3() + # i.set_db_def_value(Register.NO_INPUTS, 2) + + dt = { + 'pv1':{'manufacturer':'TSUN1','type': 'Module 100W'}, + 'pv2':{'manufacturer':'TSUN2'}, + 'pv3':{'manufacturer':'TSUN3','type': 'Module 300W'}, + 'pv4':{'type': 'Module 400W'}, + 'pv5':{}, + } + i.set_pv_module_details(dt) + assert 'TSUN1' == i.dev_value(Register.PV1_MANUFACTURER) + assert 'TSUN2' == i.dev_value(Register.PV2_MANUFACTURER) + assert 'TSUN3' == i.dev_value(Register.PV3_MANUFACTURER) + assert None == i.dev_value(Register.PV4_MANUFACTURER) + assert None == i.dev_value(Register.PV5_MANUFACTURER) + assert 'Module 100W' == i.dev_value(Register.PV1_MODEL) + assert None == i.dev_value(Register.PV2_MODEL) + assert 'Module 300W' == i.dev_value(Register.PV3_MODEL) + assert 'Module 400W' == i.dev_value(Register.PV4_MODEL) + assert None == i.dev_value(Register.PV5_MODEL) + +def test_broken_info_defs(): + i = InfosG3() + val = i.get_db_value(Register.NO_INPUTS, 666) + assert val == 666 + i.info_defs[Register.TEST_REG1] = 'test' # add a string instead of a dict + val = i.get_db_value(Register.TEST_REG1, 666) + assert val == 666 + i.set_db_def_value(Register.TEST_REG1, 2) + From ddde988e2c40238966c241dfc26f87ad78014135 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Mon, 8 Apr 2024 21:58:06 +0200 Subject: [PATCH 16/29] switch to aiomqtt version 2.0.0 --- app/requirements.txt | 2 +- app/src/mqtt.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/requirements.txt b/app/requirements.txt index fd06d2f..7558187 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,3 +1,3 @@ - aiomqtt==1.2.1 + aiomqtt==2.0.0 schema==0.7.5 aiocron==1.8 \ No newline at end of file diff --git a/app/src/mqtt.py b/app/src/mqtt.py index 413daf4..7ada981 100644 --- a/app/src/mqtt.py +++ b/app/src/mqtt.py @@ -73,17 +73,17 @@ class Mqtt(metaclass=Singleton): if self.cb_MqttIsUp: await self.cb_MqttIsUp() - async with self.__client.messages() as messages: - await self.__client.subscribe( - f"{ha['auto_conf_prefix']}" - "/status") - async for message in 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() + # async with self.__client.messages() as messages: + await self.__client.subscribe( + f"{ha['auto_conf_prefix']}" + "/status") + 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() except aiomqtt.MqttError: logger_mqtt.info(f"Connection lost; Reconnecting in {interval}" From 26b7ccd40f41fbf1f06e35f67350f4b0bc63718f Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 9 Apr 2024 00:13:45 +0200 Subject: [PATCH 17/29] switch to aiomqtt 2.0.0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 169e968..2a78aac 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

License: BSD-3-Clause Supported Python versions - Supported aiomqtt versions + Supported aiomqtt versions Supported aiocron versions Supported toml versions From 1760a764ea55942e7653b54dbdb25211795a79ed Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 9 Apr 2024 00:15:03 +0200 Subject: [PATCH 18/29] add branch name and date to version string --- app/build.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/build.sh b/app/build.sh index 25c5547..d552cb6 100755 --- a/app/build.sh +++ b/app/build.sh @@ -11,6 +11,7 @@ set -e BUILD_DATE=$(date -Iminutes) +BRANCH=$(git rev-parse --abbrev-ref HEAD) VERSION=$(git describe --tags --abbrev=0) VERSION="${VERSION:1}" arr=(${VERSION//./ }) @@ -19,7 +20,7 @@ IMAGE=tsun-gen3-proxy if [[ $1 == dev ]] || [[ $1 == rc ]] ;then IMAGE=docker.io/sallius/${IMAGE} -VERSION=${VERSION}-$1 +VERSION=${VERSION}-$1-${BRANCH}-$(date +%Y%m%d%H%M) elif [[ $1 == rel ]];then IMAGE=ghcr.io/s-allius/${IMAGE} else From 234eb26eae43cfb7e472aa95fe44605ec54fdb85 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 9 Apr 2024 00:37:30 +0200 Subject: [PATCH 19/29] remove builddate from version --- app/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.sh b/app/build.sh index d552cb6..bb89cfa 100755 --- a/app/build.sh +++ b/app/build.sh @@ -20,7 +20,7 @@ IMAGE=tsun-gen3-proxy if [[ $1 == dev ]] || [[ $1 == rc ]] ;then IMAGE=docker.io/sallius/${IMAGE} -VERSION=${VERSION}-$1-${BRANCH}-$(date +%Y%m%d%H%M) +VERSION=${VERSION}-$1-${BRANCH} elif [[ $1 == rel ]];then IMAGE=ghcr.io/s-allius/${IMAGE} else From 300196a9fc362f70573e1aa6aa61a05cae11f887 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Tue, 9 Apr 2024 00:54:58 +0200 Subject: [PATCH 20/29] migrate aiomqtt to version 2.0.0 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86483ed..100edbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Clear `Daily Generation` values at midnigth - Read pv module details from config file and use it for the Home Assistant registration see: [#43](https://github.com/s-allius/tsun-gen3-proxy/issues/43) +- migrate to aiomqtt version 2.0.0 ## [0.6.0] - 2024-04-02 From 70df843fe2f0f19ec7002a9a12247c74c6c79e50 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Wed, 10 Apr 2024 22:45:48 +0200 Subject: [PATCH 21/29] print helful messages on config errors --- app/src/config.py | 20 ++++++++++++++++++-- app/src/mqtt.py | 20 ++++++++++++++------ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/app/src/config.py b/app/src/config.py index 589bb6c..6218e65 100644 --- a/app/src/config.py +++ b/app/src/config.py @@ -13,6 +13,7 @@ class Config(): Get named parts of the config with get()''' config = {} + def_config = {} conf_schema = Schema({ 'tsun': { 'enabled': Use(bool), @@ -93,8 +94,16 @@ class Config(): # overwrite the default values, with values from # the config.toml file - with open("config/config.toml", "rb") as f: - usr_config = tomllib.load(f) + try: + with open("config/config.toml", "rb") as f: + usr_config = tomllib.load(f) + except Exception as error: + logging.error(f'Config.read: {error}') + logging.info( + '\n To create the missing config.toml file, ' + 'you can rename the template config.example.toml\n' + ' and customize it for your scenario.\n') + usr_config = def_config config['tsun'] = def_config['tsun'] | usr_config['tsun'] config['solarman'] = def_config['solarman'] | \ @@ -105,6 +114,7 @@ class Config(): usr_config['inverters'] cls.config = cls.conf_schema.validate(config) + cls.def_config = cls.conf_schema.validate(def_config) # logging.debug(f'Readed config: "{cls.config}" ') except Exception as error: @@ -120,3 +130,9 @@ class Config(): return cls.config.get(member, {}) else: return cls.config + + @classmethod + def is_default(cls, member: str) -> bool: + '''Check if the member is the default value''' + + return cls.config.get(member) == cls.def_config.get(member) diff --git a/app/src/mqtt.py b/app/src/mqtt.py index 7ada981..3a61562 100644 --- a/app/src/mqtt.py +++ b/app/src/mqtt.py @@ -24,7 +24,7 @@ class Mqtt(metaclass=Singleton): def __init__(self, cb_MqttIsUp): logger_mqtt.debug('MQTT: __init__') if cb_MqttIsUp: - self.cb_MqttIsUp = cb_MqttIsUp + self.__cb_MqttIsUp = cb_MqttIsUp loop = asyncio.get_event_loop() self.task = loop.create_task(self.__loop()) self.ha_restarts = 0 @@ -70,8 +70,8 @@ class Mqtt(metaclass=Singleton): async with self.__client: logger_mqtt.info('MQTT broker connection established') - if self.cb_MqttIsUp: - await self.cb_MqttIsUp() + if self.__cb_MqttIsUp: + await self.__cb_MqttIsUp() # async with self.__client.messages() as messages: await self.__client.subscribe( @@ -83,11 +83,19 @@ class Mqtt(metaclass=Singleton): f' {status}') if status == 'online': self.ha_restarts += 1 - await self.cb_MqttIsUp() + await self.__cb_MqttIsUp() except aiomqtt.MqttError: - logger_mqtt.info(f"Connection lost; Reconnecting in {interval}" - " seconds ...") + if Config.is_default('mqtt'): + logger_mqtt.info( + "MQTT is unconfigured; Check your config.toml!") + interval = 30 + else: + interval = 5 # Seconds + logger_mqtt.info( + f"Connection lost; Reconnecting in {interval}" + " seconds ...") + await asyncio.sleep(interval) except asyncio.CancelledError: logger_mqtt.debug("MQTT task cancelled") From d1e10b36ea3ba1444ea724e2a52b8becd39e723a Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 12 Apr 2024 18:46:22 +0200 Subject: [PATCH 22/29] add _update_header method to messages.py --- app/src/async_stream.py | 1 + app/src/messages.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/app/src/async_stream.py b/app/src/async_stream.py index 98b72a6..6c1136c 100644 --- a/app/src/async_stream.py +++ b/app/src/async_stream.py @@ -117,6 +117,7 @@ class AsyncStream(): await self.remoteStream.__async_write() if self.remoteStream: + self.remoteStream._update_header(self._forward_buffer) hex_dump_memory(logging.INFO, f'Forward to {self.remoteStream.addr}:', self._forward_buffer, diff --git a/app/src/messages.py b/app/src/messages.py index ab0b6c5..a222cc4 100644 --- a/app/src/messages.py +++ b/app/src/messages.py @@ -74,6 +74,9 @@ class Message(metaclass=IterRegistry): # to our _recv_buffer return # pragma: no cover + def _update_header(self, _forward_buffer): + return + ''' Our puplic methods ''' From edab268faab4f51af8c8ea0a4003d3d7990872d1 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 12 Apr 2024 18:47:47 +0200 Subject: [PATCH 23/29] add _update_header() to messages.py --- app/src/messages.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/messages.py b/app/src/messages.py index a222cc4..9f6efdf 100644 --- a/app/src/messages.py +++ b/app/src/messages.py @@ -75,6 +75,7 @@ class Message(metaclass=IterRegistry): return # pragma: no cover def _update_header(self, _forward_buffer): + '''callback for updating the header of the forward buffer''' return ''' From 22f68ab3304a9a1cca4e159fd3506df2981aa396 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 12 Apr 2024 18:48:22 +0200 Subject: [PATCH 24/29] beautify code --- app/src/mqtt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/mqtt.py b/app/src/mqtt.py index 3a61562..5b2de02 100644 --- a/app/src/mqtt.py +++ b/app/src/mqtt.py @@ -95,7 +95,7 @@ class Mqtt(metaclass=Singleton): logger_mqtt.info( f"Connection lost; Reconnecting in {interval}" " seconds ...") - + await asyncio.sleep(interval) except asyncio.CancelledError: logger_mqtt.debug("MQTT task cancelled") From 1d3a44c9f0043c5c1cf45c408976ae295385a8bf Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 12 Apr 2024 18:57:48 +0200 Subject: [PATCH 25/29] first self-sufficient island support - add Sequence class to handle the sequence of packets - send response for received packets directly - don't forward responses anymore - addapt tests to new behavior --- app/src/gen3plus/solarman_v5.py | 125 +++++++++++++++++++++----------- app/tests/test_solarman.py | 109 +++++++++++++++++----------- 2 files changed, 151 insertions(+), 83 deletions(-) diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index fc189b3..86b64f1 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -1,7 +1,7 @@ import struct # import json import logging -# import time +import time from datetime import datetime if __name__ == "app.src.gen3plus.solarman_v5": @@ -19,6 +19,32 @@ else: # pragma: no cover logger = logging.getLogger('msg') +class Sequence(): + def __init__(self, server_side: bool): + self.rcv_idx = 0 + self.snd_idx = 0 + self.server_side = server_side + + def set_recv(self, val: int): + if self.server_side: + self.rcv_idx = val >> 8 + self.snd_idx = val & 0xff + else: + self.rcv_idx = val & 0xff + self.snd_idx = val >> 8 + + def get_send(self): + self.snd_idx += 1 + self.snd_idx &= 0xff + if self.server_side: + return (self.rcv_idx << 8) | self.snd_idx + else: + return (self.snd_idx << 8) | self.rcv_idx + + def __str__(self): + return f'{self.rcv_idx:02x}:{self.snd_idx:02x}' + + class SolarmanV5(Message): def __init__(self, server_side: bool): @@ -26,7 +52,7 @@ class SolarmanV5(Message): self.header_len = 11 # overwrite construcor in class Message self.control = 0 - self.serial = 0 + self.seq = Sequence(server_side) self.snr = 0 self.db = InfosG3P() self.switch = { @@ -160,6 +186,13 @@ class SolarmanV5(Message): type += 'S' return switch.get(type, '???') + def _timestamp(self): # pragma: no cover + # utc as epoche + return int(time.time()) + + def _heartbeat(self) -> int: + return 60 + def __parse_header(self, buf: bytes, buf_len: int) -> None: if (buf_len < self.header_len): # enough bytes for complete header? @@ -168,10 +201,10 @@ class SolarmanV5(Message): result = struct.unpack_from(' None: + '''build header for new transmit message''' + self.send_msg_ofs = len(self._send_buffer) + + self._send_buffer += struct.pack( + ' None: + '''finish the transmit message, set lenght and checksum''' + _len = len(self._send_buffer) - self.send_msg_ofs + struct.pack_into(' None: fnc = self.switch.get(self.control, self.msg_unknown) if self.unique_id: @@ -220,41 +284,7 @@ class SolarmanV5(Message): self._recv_buffer = self._recv_buffer[(self.header_len + self.data_len+2):] self.header_valid = False - ''' - def modbus(self, data): - POLY = 0xA001 - crc = 0xFFFF - for byte in data: - crc ^= byte - for _ in range(8): - crc = ((crc >> 1) ^ POLY - if (crc & 0x0001) - else crc >> 1) - return crc - - def validate_modbus_crc(self, frame): - # Calculate crc with all but the last 2 bytes of - # the frame (they contain the crc) - calc_crc = 0xFFFF - for pos in frame[:-2]: - calc_crc ^= pos - for i in range(8): - if (calc_crc & 1) != 0: - calc_crc >>= 1 - calc_crc ^= 0xA001 # bitwise 'or' with modbus magic - # number (0xa001 == bitwise - # reverse of 0x8005) - else: - calc_crc >>= 1 - - # Compare calculated crc with the one supplied in the frame.... - frame_crc, = struct.unpack(' int: + return heartbeat + def append_msg(self, msg): self.__msg += msg @@ -42,9 +54,6 @@ class MemoryStream(SolarmanV5): pass return copied_bytes - def _timestamp(self): - return 1700260990000 - def _SolarmanV5__flush_recv_msg(self) -> None: super()._SolarmanV5__flush_recv_msg() self.msg_count += 1 @@ -60,6 +69,16 @@ def get_inv_no() -> bytes: def get_invalid_sn(): return b'R170000000000002' +def total(): + ts = timestamp + # convert int to little-endian bytes + return struct.pack(' Date: Fri, 12 Apr 2024 19:38:06 +0200 Subject: [PATCH 26/29] erase trailing whitespace --- app/src/gen3plus/solarman_v5.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index 86b64f1..ac2ce61 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -257,7 +257,7 @@ class SolarmanV5(Message): self._send_buffer += struct.pack(' Date: Fri, 12 Apr 2024 19:39:34 +0200 Subject: [PATCH 27/29] adapt feature description --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a78aac..697ea87 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ If you use a Pi-hole, you can also store the host entry in the Pi-hole. - supports TSUN GEN3 inverters: TSOL-MS800, MS700, MS600, MS400, MS350 and MS300 - `MQTT` support - `Home-Assistant` auto-discovery support -- Self-sufficient island operation without internet (for TSUN GEN3 PLUS inverters in preparation) +- Self-sufficient island operation without internet - runs in a non-root Docker Container ## Home Assistant Screenshots From 74ac6c66661c5b0663b8eb26229d6d317d53acd2 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 12 Apr 2024 20:50:57 +0200 Subject: [PATCH 28/29] fix at_commit_message(); code cleanup --- app/src/gen3plus/solarman_v5.py | 159 +++++++++++++++++--------------- app/tests/test_solarman.py | 15 ++- 2 files changed, 97 insertions(+), 77 deletions(-) diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index ac2ce61..20c85c2 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -58,10 +58,10 @@ class SolarmanV5(Message): self.switch = { 0x4210: self.msg_data_ind, # real time data - 0x1210: self.msg_data_rsp, # at least every 5 minutes + 0x1210: self.msg_response, # at least every 5 minutes 0x4710: self.msg_hbeat_ind, # heatbeat - 0x1710: self.msg_hbeat_rsp, # every 2 minutes + 0x1710: self.msg_response, # every 2 minutes # every 3 hours comes a sync seuqence: # 00:00:00 0x4110 device data ftype: 0x02 @@ -72,18 +72,18 @@ class SolarmanV5(Message): # 00:00:07 0x4310 wifi data ftype: 0x01 sub-id 0x0018: 0c # noqa: E501 # 00:00:08 0x4810 options? ftype: 0x01 - 0x4110: self.msg_dev_ind, # device data, sync start - 0x1110: self.msg_dev_rsp, # every 3 hours + 0x4110: self.msg_dev_ind, # device data, sync start + 0x1110: self.msg_response, # every 3 hours - 0x4310: self.msg_forward, # regulary after 3-6 hours - 0x1310: self.msg_forward, - 0x4810: self.msg_forward, # sync end - 0x1810: self.msg_forward, + 0x4310: self.msg_sync_start, # regulary after 3-6 hours + 0x1310: self.msg_response, + 0x4810: self.msg_sync_end, # sync end + 0x1810: self.msg_response, # # AT cmd 0x4510: self.at_command_ind, # from server - 0x1510: self.msg_forward, # from inverter + 0x1510: self.msg_response, # from inverter } ''' @@ -285,62 +285,15 @@ class SolarmanV5(Message): self.data_len+2):] self.header_valid = False - ''' - Message handler methods - ''' - def msg_unknown(self): - logger.warning(f"Unknow Msg: ID:{int(self.control):#04x}") - self.inc_counter('Unknown_Msg') - self.msg_forward() - - def msg_forward(self): - self.forward(self._recv_buffer, self.header_len+self.data_len+2) - - def msg_dev_ind(self): - data = self._recv_buffer[self.header_len:] - result = struct.unpack_from(' Date: Sat, 13 Apr 2024 20:18:44 +0200 Subject: [PATCH 29/29] experimental AT cmd handler and tests --- app/src/gen3plus/solarman_v5.py | 15 +++++++++++---- app/tests/test_solarman.py | 26 +++++++++++++++++++++----- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index 20c85c2..5c8e588 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -81,9 +81,9 @@ class SolarmanV5(Message): 0x1810: self.msg_response, # - # AT cmd - 0x4510: self.at_command_ind, # from server - 0x1510: self.msg_response, # from inverter + # MODbus or AT cmd + 0x4510: self.msg_command_req, # from server + 0x1510: self.msg_response, # from inverter } ''' @@ -292,6 +292,13 @@ class SolarmanV5(Message): self._heartbeat()) self.__finish_send_msg() + def send_at_cmd(self, AT_cmd: str) -> None: + self.__build_header(0x4510) + self._send_buffer += struct.pack(f'