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 @@
-
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 @@
-
+
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 @@
-
+
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'