From d1e10b36ea3ba1444ea724e2a52b8becd39e723a Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 12 Apr 2024 18:46:22 +0200 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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