From 87abc88cf9f54e7e64032c13f94b84c95a060d97 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Sun, 22 Dec 2024 02:41:14 +0100 Subject: [PATCH 01/27] feat: Implemented response to heartbeat packets --- pysolarmanv5/pysolarmanv5.py | 35 +++++++++++++++++++++--- pysolarmanv5/pysolarmanv5_async.py | 43 +++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 593b5a8..5154037 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -1,12 +1,13 @@ """pysolarmanv5.py""" +import time import errno import queue import struct import socket import logging -import selectors import platform +import selectors from threading import Thread, Event from multiprocessing import Queue @@ -299,9 +300,9 @@ def _send_receive_v5_frame(self, data_logging_stick_frame): return v5_response def _received_frame_is_valid(self, frame): - """Check that the frame is valid and that the serial number of the received + """ + Check that the frame is valid and that the serial number of the received frame matches with the last sent one. - Ignore also any frames with control code 0x4710 (counter frame). """ if not frame.startswith(self.v5_start): self.log.debug("[%s] V5_MISMATCH: %s", self.serial, frame.hex(" ")) @@ -309,8 +310,32 @@ def _received_frame_is_valid(self, frame): if frame[5] != self.sequence_number: self.log.debug("[%s] V5_SEQ_NO_MISMATCH: %s", self.serial, frame.hex(" ")) return False + return True + + def _handle_protocol_frame(self, frame): + """ + Handles protocol frames with control code 0x4710 (heartbeat frame). + """ if frame.startswith(self.v5_start + b"\x01\x00\x10\x47"): - self.log.debug("[%s] COUNTER: %s", self.serial, frame.hex(" ")) + self.log.debug("[%s] V5_HEARTBEAT: %s", self.serial, frame.hex(" ")) + response_frame = bytearray( + self.v5_start + + struct.pack(" None: """ Socket reader loop with extra logic when auto-reconnect is enabled @@ -185,6 +224,8 @@ async def _conn_keeper(self) -> None: break if not self._received_frame_is_valid(data): continue + if not await self._handle_protocol_frame(data): + continue if self.data_wanted_ev.is_set(): self._send_data(data) else: From 22b64caff3f18796a657b1bac8d627072a8e7026 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Sun, 22 Dec 2024 02:50:00 +0100 Subject: [PATCH 02/27] refactor: Add _v5_heartbeat_response_frame --- pysolarmanv5/pysolarmanv5.py | 36 +++++++++++++++++------------- pysolarmanv5/pysolarmanv5_async.py | 16 +------------ 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 5154037..eb86fc2 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -266,6 +266,26 @@ def _v5_frame_decoder(self, v5_frame): return modbus_frame + def _v5_heartbeat_response_frame(self, heartbeat_frame): + """ + Creates response to 0x4710 (heartbeat frame) + """ + response_frame = bytearray( + self.v5_start + + struct.pack(" Date: Sun, 22 Dec 2024 03:01:35 +0100 Subject: [PATCH 03/27] refactor: V5_HEARTBEAT error message --- pysolarmanv5/pysolarmanv5_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysolarmanv5/pysolarmanv5_async.py b/pysolarmanv5/pysolarmanv5_async.py index e0c36a4..58dafc3 100644 --- a/pysolarmanv5/pysolarmanv5_async.py +++ b/pysolarmanv5/pysolarmanv5_async.py @@ -181,7 +181,7 @@ async def _handle_protocol_frame(self, frame): f"[{self.serial}] V5_HEARTBEAT error: {type(e).__name__}{f': {e}' if f'{e}' else ''}" ) except Exception as e: - self.log.exception("[%s] Send/Receive error: %s", self.serial, e) + self.log.exception("[%s] V5_HEARTBEAT error: %s", self.serial, e) return False return True From 1afac93e199d506bd87623af26fa9d8c5dbe741d Mon Sep 17 00:00:00 2001 From: David Rapan Date: Sun, 22 Dec 2024 15:08:33 +0100 Subject: [PATCH 04/27] refactor: Rename _v5_heartbeat_response_frame to _v5_time_response_frame --- pysolarmanv5/pysolarmanv5.py | 4 ++-- pysolarmanv5/pysolarmanv5_async.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index eb86fc2..eb4cf49 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -266,7 +266,7 @@ def _v5_frame_decoder(self, v5_frame): return modbus_frame - def _v5_heartbeat_response_frame(self, heartbeat_frame): + def _v5_time_response_frame(self, heartbeat_frame): """ Creates response to 0x4710 (heartbeat frame) """ @@ -338,7 +338,7 @@ def _handle_protocol_frame(self, frame): """ if frame.startswith(self.v5_start + b"\x01\x00\x10\x47"): self.log.debug("[%s] V5_HEARTBEAT: %s", self.serial, frame.hex(" ")) - response_frame = self._v5_heartbeat_response_frame(frame) + response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_HEARTBEAT RESP: %s", self.serial, response_frame.hex(" ")) if self._reader_thr.is_alive(): self.sock.sendall(response_frame) diff --git a/pysolarmanv5/pysolarmanv5_async.py b/pysolarmanv5/pysolarmanv5_async.py index 58dafc3..b965376 100644 --- a/pysolarmanv5/pysolarmanv5_async.py +++ b/pysolarmanv5/pysolarmanv5_async.py @@ -167,7 +167,7 @@ async def _handle_protocol_frame(self, frame): """ if frame.startswith(self.v5_start + b"\x01\x00\x10\x47"): self.log.debug("[%s] V5_HEARTBEAT: %s", self.serial, frame.hex(" ")) - response_frame = self._v5_heartbeat_response_frame(frame) + response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_HEARTBEAT RESP: %s", self.serial, response_frame.hex(" ")) try: self.writer.write(response_frame) From 8b23962d2b5a6000e041e54064340c0467567be8 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Sun, 22 Dec 2024 15:18:29 +0100 Subject: [PATCH 05/27] feat: Add V5_HANDSHAKE handling for server mode --- pysolarmanv5/pysolarmanv5.py | 9 ++++++++- pysolarmanv5/pysolarmanv5_async.py | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index eb4cf49..67f904f 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -334,12 +334,19 @@ def _received_frame_is_valid(self, frame): def _handle_protocol_frame(self, frame): """ - Handles protocol frames with control code 0x4710 (heartbeat frame). + Handles protocol frames with control code 0x4110 (handshake) and 0x4710 (heartbeat). """ + response_frame = None + + if frame[4] == 0x41: + self.log.debug("[%s] V5_HANDSHAKE: %s", self.serial, frame.hex(" ")) + response_frame = self._v5_time_response_frame(frame) + self.log.debug("[%s] V5_HANDSHAKE RESP: %s", self.serial, response_frame.hex(" ")) if frame.startswith(self.v5_start + b"\x01\x00\x10\x47"): self.log.debug("[%s] V5_HEARTBEAT: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_HEARTBEAT RESP: %s", self.serial, response_frame.hex(" ")) + if response_frame: if self._reader_thr.is_alive(): self.sock.sendall(response_frame) return False diff --git a/pysolarmanv5/pysolarmanv5_async.py b/pysolarmanv5/pysolarmanv5_async.py index b965376..b0d137b 100644 --- a/pysolarmanv5/pysolarmanv5_async.py +++ b/pysolarmanv5/pysolarmanv5_async.py @@ -163,12 +163,19 @@ def _send_data(self, data: bytes): async def _handle_protocol_frame(self, frame): """ - Handles protocol frames with control code 0x4710 (heartbeat frame). + Handles protocol frames with control code 0x4110 (handshake) and 0x4710 (heartbeat). """ + response_frame = None + + if frame[4] == 0x41: + self.log.debug("[%s] V5_HANDSHAKE: %s", self.serial, frame.hex(" ")) + response_frame = self._v5_time_response_frame(frame) + self.log.debug("[%s] V5_HANDSHAKE RESP: %s", self.serial, response_frame.hex(" ")) if frame.startswith(self.v5_start + b"\x01\x00\x10\x47"): self.log.debug("[%s] V5_HEARTBEAT: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_HEARTBEAT RESP: %s", self.serial, response_frame.hex(" ")) + if response_frame: try: self.writer.write(response_frame) await self.writer.drain() From 1424502d02a349af18e78f5bc28a2cf8dd5dcb61 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Sun, 22 Dec 2024 15:44:18 +0100 Subject: [PATCH 06/27] feat: Add V5_DATA and V5_WIFI handling for server mode --- pysolarmanv5/pysolarmanv5.py | 13 ++++++++++--- pysolarmanv5/pysolarmanv5_async.py | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 67f904f..697dfaa 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -334,15 +334,22 @@ def _received_frame_is_valid(self, frame): def _handle_protocol_frame(self, frame): """ - Handles protocol frames with control code 0x4110 (handshake) and 0x4710 (heartbeat). + Handles frames with control code 0x41 (handshake), 0x42 (data), 0x43 (wifi) and 0x47 (heartbeat). """ response_frame = None - if frame[4] == 0x41: self.log.debug("[%s] V5_HANDSHAKE: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_HANDSHAKE RESP: %s", self.serial, response_frame.hex(" ")) - if frame.startswith(self.v5_start + b"\x01\x00\x10\x47"): + if frame[4] == 0x42: + self.log.debug("[%s] V5_DATA: %s", self.serial, frame.hex(" ")) + response_frame = self._v5_time_response_frame(frame) + self.log.debug("[%s] V5_DATA RESP: %s", self.serial, response_frame.hex(" ")) + if frame[4] == 0x43: + self.log.debug("[%s] V5_WIFI: %s", self.serial, frame.hex(" ")) + response_frame = self._v5_time_response_frame(frame) + self.log.debug("[%s] V5_WIFI RESP: %s", self.serial, response_frame.hex(" ")) + if frame[4] == 0x47: self.log.debug("[%s] V5_HEARTBEAT: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_HEARTBEAT RESP: %s", self.serial, response_frame.hex(" ")) diff --git a/pysolarmanv5/pysolarmanv5_async.py b/pysolarmanv5/pysolarmanv5_async.py index b0d137b..214e688 100644 --- a/pysolarmanv5/pysolarmanv5_async.py +++ b/pysolarmanv5/pysolarmanv5_async.py @@ -163,15 +163,22 @@ def _send_data(self, data: bytes): async def _handle_protocol_frame(self, frame): """ - Handles protocol frames with control code 0x4110 (handshake) and 0x4710 (heartbeat). + Handles frames with control code 0x41 (handshake), 0x42 (data), 0x43 (wifi) and 0x47 (heartbeat). """ response_frame = None - if frame[4] == 0x41: self.log.debug("[%s] V5_HANDSHAKE: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_HANDSHAKE RESP: %s", self.serial, response_frame.hex(" ")) - if frame.startswith(self.v5_start + b"\x01\x00\x10\x47"): + if frame[4] == 0x42: + self.log.debug("[%s] V5_DATA: %s", self.serial, frame.hex(" ")) + response_frame = self._v5_time_response_frame(frame) + self.log.debug("[%s] V5_DATA RESP: %s", self.serial, response_frame.hex(" ")) + if frame[4] == 0x43: + self.log.debug("[%s] V5_WIFI: %s", self.serial, frame.hex(" ")) + response_frame = self._v5_time_response_frame(frame) + self.log.debug("[%s] V5_WIFI RESP: %s", self.serial, response_frame.hex(" ")) + if frame[4] == 0x47: self.log.debug("[%s] V5_HEARTBEAT: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_HEARTBEAT RESP: %s", self.serial, response_frame.hex(" ")) From f16ac48d0de1efa3fd71e07d847912e7aefc77e2 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Sun, 22 Dec 2024 17:10:49 +0100 Subject: [PATCH 07/27] refactor: _handle_protocol_frame through _received_frame_response --- pysolarmanv5/pysolarmanv5.py | 16 +++++++++++++--- pysolarmanv5/pysolarmanv5_async.py | 25 ++++--------------------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 697dfaa..7f7e798 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -332,9 +332,9 @@ def _received_frame_is_valid(self, frame): return False return True - def _handle_protocol_frame(self, frame): + def _received_frame_response(self, frame): """ - Handles frames with control code 0x41 (handshake), 0x42 (data), 0x43 (wifi) and 0x47 (heartbeat). + Return response to frames with control codes 0x41 (handshake), 0x42 (data), 0x43 (wifi) and 0x47 (heartbeat). """ response_frame = None if frame[4] == 0x41: @@ -353,7 +353,17 @@ def _handle_protocol_frame(self, frame): self.log.debug("[%s] V5_HEARTBEAT: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_HEARTBEAT RESP: %s", self.serial, response_frame.hex(" ")) - if response_frame: + if frame[4] == 0x48: + self.log.debug("[%s] V5_REPORT: %s", self.serial, frame.hex(" ")) + response_frame = self._v5_time_response_frame(frame) + self.log.debug("[%s] V5_REPORT RESP: %s", self.serial, response_frame.hex(" ")) + return response_frame + + def _handle_protocol_frame(self, frame): + """ + Handles frames with known control codes :func:`_received_frame_response() ` + """ + if (response_frame := self._received_frame_response(frame)) is not None: if self._reader_thr.is_alive(): self.sock.sendall(response_frame) return False diff --git a/pysolarmanv5/pysolarmanv5_async.py b/pysolarmanv5/pysolarmanv5_async.py index 214e688..4e1fe3e 100644 --- a/pysolarmanv5/pysolarmanv5_async.py +++ b/pysolarmanv5/pysolarmanv5_async.py @@ -163,26 +163,9 @@ def _send_data(self, data: bytes): async def _handle_protocol_frame(self, frame): """ - Handles frames with control code 0x41 (handshake), 0x42 (data), 0x43 (wifi) and 0x47 (heartbeat). + Handles frames with known control codes :func:`_received_frame_response() ` """ - response_frame = None - if frame[4] == 0x41: - self.log.debug("[%s] V5_HANDSHAKE: %s", self.serial, frame.hex(" ")) - response_frame = self._v5_time_response_frame(frame) - self.log.debug("[%s] V5_HANDSHAKE RESP: %s", self.serial, response_frame.hex(" ")) - if frame[4] == 0x42: - self.log.debug("[%s] V5_DATA: %s", self.serial, frame.hex(" ")) - response_frame = self._v5_time_response_frame(frame) - self.log.debug("[%s] V5_DATA RESP: %s", self.serial, response_frame.hex(" ")) - if frame[4] == 0x43: - self.log.debug("[%s] V5_WIFI: %s", self.serial, frame.hex(" ")) - response_frame = self._v5_time_response_frame(frame) - self.log.debug("[%s] V5_WIFI RESP: %s", self.serial, response_frame.hex(" ")) - if frame[4] == 0x47: - self.log.debug("[%s] V5_HEARTBEAT: %s", self.serial, frame.hex(" ")) - response_frame = self._v5_time_response_frame(frame) - self.log.debug("[%s] V5_HEARTBEAT RESP: %s", self.serial, response_frame.hex(" ")) - if response_frame: + if (response_frame := self._received_frame_response(frame)) is not None: try: self.writer.write(response_frame) await self.writer.drain() @@ -192,10 +175,10 @@ async def _handle_protocol_frame(self, frame): if isinstance(e, OSError) and e.errno == errno.EHOSTUNREACH: e = TimeoutError self.log.debug( # pylint: disable=logging-fstring-interpolation - f"[{self.serial}] V5_HEARTBEAT error: {type(e).__name__}{f': {e}' if f'{e}' else ''}" + f"[{self.serial}] V5_PROTOCOL error: {type(e).__name__}{f': {e}' if f'{e}' else ''}" ) except Exception as e: - self.log.exception("[%s] V5_HEARTBEAT error: %s", self.serial, e) + self.log.exception("[%s] V5_PROTOCOL error: %s", self.serial, e) return False return True From 9163dfdd601a6b1e3a68f0aa67a1facc209b2b2f Mon Sep 17 00:00:00 2001 From: David Rapan Date: Sun, 22 Dec 2024 17:32:25 +0100 Subject: [PATCH 08/27] refactor: Add variable continue to _received_frame_response --- pysolarmanv5/pysolarmanv5.py | 14 ++++++++++---- pysolarmanv5/pysolarmanv5_async.py | 6 +++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 7f7e798..27be492 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -336,38 +336,44 @@ def _received_frame_response(self, frame): """ Return response to frames with control codes 0x41 (handshake), 0x42 (data), 0x43 (wifi) and 0x47 (heartbeat). """ + do_continue = True response_frame = None if frame[4] == 0x41: + do_continue = False self.log.debug("[%s] V5_HANDSHAKE: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_HANDSHAKE RESP: %s", self.serial, response_frame.hex(" ")) if frame[4] == 0x42: + do_continue = False # Maybe True and thus process the packet in the future? self.log.debug("[%s] V5_DATA: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_DATA RESP: %s", self.serial, response_frame.hex(" ")) if frame[4] == 0x43: + do_continue = False self.log.debug("[%s] V5_WIFI: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_WIFI RESP: %s", self.serial, response_frame.hex(" ")) if frame[4] == 0x47: + do_continue = False self.log.debug("[%s] V5_HEARTBEAT: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_HEARTBEAT RESP: %s", self.serial, response_frame.hex(" ")) if frame[4] == 0x48: + do_continue = False self.log.debug("[%s] V5_REPORT: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_REPORT RESP: %s", self.serial, response_frame.hex(" ")) - return response_frame + return do_continue, response_frame def _handle_protocol_frame(self, frame): """ Handles frames with known control codes :func:`_received_frame_response() ` """ - if (response_frame := self._received_frame_response(frame)) is not None: + do_continue, response_frame = self._received_frame_response(frame) + if response_frame is not None: if self._reader_thr.is_alive(): self.sock.sendall(response_frame) - return False - return True + return do_continue def _data_receiver(self): self._poll.register(self.sock.fileno(), selectors.EVENT_READ) diff --git a/pysolarmanv5/pysolarmanv5_async.py b/pysolarmanv5/pysolarmanv5_async.py index 4e1fe3e..592f808 100644 --- a/pysolarmanv5/pysolarmanv5_async.py +++ b/pysolarmanv5/pysolarmanv5_async.py @@ -165,7 +165,8 @@ async def _handle_protocol_frame(self, frame): """ Handles frames with known control codes :func:`_received_frame_response() ` """ - if (response_frame := self._received_frame_response(frame)) is not None: + do_continue, response_frame = self._received_frame_response(frame) + if response_frame is not None: try: self.writer.write(response_frame) await self.writer.drain() @@ -179,8 +180,7 @@ async def _handle_protocol_frame(self, frame): ) except Exception as e: self.log.exception("[%s] V5_PROTOCOL error: %s", self.serial, e) - return False - return True + return do_continue async def _conn_keeper(self) -> None: """ From ac76b6c69f639b62196e9382414c44b3f761cec0 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Tue, 24 Dec 2024 04:40:42 +0100 Subject: [PATCH 09/27] fix: _v5_time_response_frame argument name --- pysolarmanv5/pysolarmanv5.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 27be492..4bc085a 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -266,14 +266,14 @@ def _v5_frame_decoder(self, v5_frame): return modbus_frame - def _v5_time_response_frame(self, heartbeat_frame): + def _v5_time_response_frame(self, frame): """ Creates response to 0x4710 (heartbeat frame) """ response_frame = bytearray( self.v5_start + struct.pack(" Date: Wed, 25 Dec 2024 20:31:30 +0100 Subject: [PATCH 10/27] refactor: _v5_time_response_frame and _received_frame_response method infos --- pysolarmanv5/pysolarmanv5.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 4bc085a..bb52a2c 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -268,7 +268,7 @@ def _v5_frame_decoder(self, v5_frame): def _v5_time_response_frame(self, frame): """ - Creates response to 0x4710 (heartbeat frame) + Creates time response frame """ response_frame = bytearray( self.v5_start @@ -334,7 +334,7 @@ def _received_frame_is_valid(self, frame): def _received_frame_response(self, frame): """ - Return response to frames with control codes 0x41 (handshake), 0x42 (data), 0x43 (wifi) and 0x47 (heartbeat). + Return response to frames with control codes 0x41 (handshake), 0x42 (data), 0x43 (wifi), 0x47 (heartbeat) and 0x48 (report) """ do_continue = True response_frame = None From e5d4d940b9f92944ad1e41d0d157657feb6e1f20 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Thu, 26 Dec 2024 03:37:58 +0100 Subject: [PATCH 11/27] feat: Add control codes --- pysolarmanv5/pysolarmanv5.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index bb52a2c..82e2ae0 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -3,6 +3,7 @@ import time import errno import queue +import types import struct import socket import logging @@ -22,6 +23,14 @@ _WIN_PLATFORM = platform.system() == "Windows" +CONTROL = types.SimpleNamespace() +CONTROL.HANDSHAKE = 0x41 +CONTROL.DATA = 0x42 +CONTROL.INFO = 0x43 +CONTROL.HEARTBEAT = 0x47 +CONTROL.REPORT = 0x48 + + class V5FrameError(Exception): """V5 Frame Validation Error""" @@ -338,27 +347,27 @@ def _received_frame_response(self, frame): """ do_continue = True response_frame = None - if frame[4] == 0x41: + if frame[4] == CONTROL.HANDSHAKE: do_continue = False self.log.debug("[%s] V5_HANDSHAKE: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_HANDSHAKE RESP: %s", self.serial, response_frame.hex(" ")) - if frame[4] == 0x42: + if frame[4] == CONTROL.DATA: do_continue = False # Maybe True and thus process the packet in the future? self.log.debug("[%s] V5_DATA: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_DATA RESP: %s", self.serial, response_frame.hex(" ")) - if frame[4] == 0x43: + if frame[4] == CONTROL.INFO: do_continue = False - self.log.debug("[%s] V5_WIFI: %s", self.serial, frame.hex(" ")) + self.log.debug("[%s] V5_INFO: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) - self.log.debug("[%s] V5_WIFI RESP: %s", self.serial, response_frame.hex(" ")) - if frame[4] == 0x47: + self.log.debug("[%s] V5_INFO RESP: %s", self.serial, response_frame.hex(" ")) + if frame[4] == CONTROL.HEARTBEAT: do_continue = False self.log.debug("[%s] V5_HEARTBEAT: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_HEARTBEAT RESP: %s", self.serial, response_frame.hex(" ")) - if frame[4] == 0x48: + if frame[4] == CONTROL.REPORT: do_continue = False self.log.debug("[%s] V5_REPORT: %s", self.serial, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) From 86398b537413ca73d1ae281cd19412f9a9c19ed0 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Thu, 26 Dec 2024 04:23:58 +0100 Subject: [PATCH 12/27] refactor: Encoder & Decoder to use control codes --- pysolarmanv5/pysolarmanv5.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 82e2ae0..dabd9f8 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -27,6 +27,7 @@ CONTROL.HANDSHAKE = 0x41 CONTROL.DATA = 0x42 CONTROL.INFO = 0x43 +CONTROL.REQUEST = 0x45 CONTROL.HEARTBEAT = 0x47 CONTROL.REPORT = 0x48 @@ -120,7 +121,8 @@ def __init__(self, address, serial, **kwargs): # Define and construct V5 request frame structure. self.v5_start = bytes.fromhex("A5") self.v5_length = bytes.fromhex("0000") # placeholder value - self.v5_controlcode = struct.pack(" Date: Thu, 26 Dec 2024 04:27:36 +0100 Subject: [PATCH 13/27] refactor: Extract _v5_header from _v5_frame_encoder and thus reuse header encoding --- pysolarmanv5/pysolarmanv5.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index dabd9f8..cfd3cb6 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -159,6 +159,20 @@ def _calculate_v5_frame_checksum(frame): checksum += frame[i] & 0xFF return int(checksum & 0xFF) + def _v5_header(self, length: int, control: int, seq: bytes) -> bytes: + """ + Construct V5 header + + """ + return bytearray( + self.v5_start + + struct.pack(" Date: Thu, 26 Dec 2024 04:31:40 +0100 Subject: [PATCH 14/27] fix: _v5_frame_decoder control condition --- pysolarmanv5/pysolarmanv5.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index cfd3cb6..1775d32 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -146,7 +146,8 @@ def __init__(self, address, serial, **kwargs): @staticmethod def _calculate_v5_frame_checksum(frame): - """Calculate checksum on all frame bytes except head, end and checksum + """ + Calculate checksum on all frame bytes except head, end and checksum :param frame: V5 frame :type frame: bytes @@ -174,7 +175,8 @@ def _v5_header(self, length: int, control: int, seq: bytes) -> bytes: ) def _get_next_sequence_number(self): - """Get the next sequence number for use in outgoing packets + """ + Get the next sequence number for use in outgoing packets If ``sequence_number`` is None, generate a random int as initial value. @@ -189,7 +191,8 @@ def _get_next_sequence_number(self): return self.sequence_number def _v5_frame_encoder(self, modbus_frame): - """Take a modbus RTU frame and encode it in a V5 data logging stick frame + """ + Take a modbus RTU frame and encode it in a V5 data logging stick frame :param modbus_frame: Modbus RTU frame :type modbus_frame: bytes @@ -197,7 +200,6 @@ def _v5_frame_encoder(self, modbus_frame): :rtype: bytearray """ - self.v5_length = struct.pack("` + """ do_continue, response_frame = self._received_frame_response(frame) if response_frame is not None: From 1889f9d1cd6fee09ae16a1214f94f40b20cad4c8 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Thu, 26 Dec 2024 04:38:26 +0100 Subject: [PATCH 15/27] refactor: Add _get_response_code to get control code from response control code --- pysolarmanv5/pysolarmanv5.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 1775d32..31e2468 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -174,6 +174,13 @@ def _v5_header(self, length: int, control: int, seq: bytes) -> bytes: + self.v5_loggerserial ) + def _get_response_code(self, control): + """ + Get response control code from request control code + + """ + return control - 0x30 + def _get_next_sequence_number(self): """ Get the next sequence number for use in outgoing packets @@ -272,7 +279,7 @@ def _v5_frame_decoder(self, v5_frame): raise V5FrameError("V5 frame contains invalid sequence number") if v5_frame[7:11] != self.v5_loggerserial: raise V5FrameError("V5 frame contains incorrect data logger serial number") - if v5_frame[3] != self.v5_magic or v5_frame[4] != CONTROL.REQUEST - 0x30: + if v5_frame[3] != self.v5_magic or v5_frame[4] != self._get_response_code(CONTROL.REQUEST): raise V5FrameError("V5 frame contains incorrect control code") if v5_frame[11] != int("02", 16): raise V5FrameError("V5 frame contains invalid frametype") @@ -293,14 +300,13 @@ def _v5_time_response_frame(self, frame): Creates time response frame """ - response_frame = self._v5_header(10, frame[4], frame[5:7]) + bytearray( + response_frame = self._v5_header(10, self._get_response_code(frame[4]), frame[5:7]) + bytearray( + struct.pack(" Date: Thu, 26 Dec 2024 05:01:39 +0100 Subject: [PATCH 16/27] fix: V5 frame encoder --- pysolarmanv5/pysolarmanv5.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 31e2468..e9e9b2f 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -122,7 +122,6 @@ def __init__(self, address, serial, **kwargs): self.v5_start = bytes.fromhex("A5") self.v5_length = bytes.fromhex("0000") # placeholder value self.v5_magic = bytes.fromhex("10") - self.v5_control = struct.pack(" Date: Thu, 26 Dec 2024 05:11:20 +0100 Subject: [PATCH 17/27] fix: v5_magic as int --- pysolarmanv5/pysolarmanv5.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index e9e9b2f..ca67e62 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -279,7 +279,7 @@ def _v5_frame_decoder(self, v5_frame): raise V5FrameError("V5 frame contains invalid sequence number") if v5_frame[7:11] != self.v5_loggerserial: raise V5FrameError("V5 frame contains incorrect data logger serial number") - if v5_frame[3] != self.v5_magic or v5_frame[4] != self._get_response_code(CONTROL.REQUEST): + if v5_frame[4] != self._get_response_code(CONTROL.REQUEST): raise V5FrameError("V5 frame contains incorrect control code") if v5_frame[11] != int("02", 16): raise V5FrameError("V5 frame contains invalid frametype") From cbd2e67b01d17a560cb0fca15d8425edc48d2ff3 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Thu, 26 Dec 2024 05:18:51 +0100 Subject: [PATCH 18/27] refactor: Extract _v5_trailer from _v5_frame_encoder to be reused --- pysolarmanv5/pysolarmanv5.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index ca67e62..43a9fec 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -159,7 +159,7 @@ def _calculate_v5_frame_checksum(frame): checksum += frame[i] & 0xFF return int(checksum & 0xFF) - def _v5_header(self, length: int, control: int, seq: bytes) -> bytes: + def _v5_header(self, length: int, control: int, seq: bytes) -> bytearray: """ Construct V5 header @@ -173,14 +173,21 @@ def _v5_header(self, length: int, control: int, seq: bytes) -> bytes: + self.v5_loggerserial ) - def _get_response_code(self, control): + def _v5_trailer(self) -> bytearray: + """ + Construct V5 trailer + + """ + return bytearray(self.v5_checksum + self.v5_end) + + def _get_response_code(self, control) -> int: """ Get response control code from request control code """ return control - 0x30 - def _get_next_sequence_number(self): + def _get_next_sequence_number(self) -> int: """ Get the next sequence number for use in outgoing packets @@ -196,7 +203,7 @@ def _get_next_sequence_number(self): self.sequence_number = (self.sequence_number + 1) & 0xFF return self.sequence_number - def _v5_frame_encoder(self, modbus_frame): + def _v5_frame_encoder(self, modbus_frame) -> bytearray: """ Take a modbus RTU frame and encode it in a V5 data logging stick frame @@ -221,14 +228,12 @@ def _v5_frame_encoder(self, modbus_frame): + modbus_frame ) - v5_trailer = bytearray(self.v5_checksum + self.v5_end) - - v5_frame = v5_header + v5_payload + v5_trailer + v5_frame = v5_header + v5_payload + self._v5_trailer() v5_frame[len(v5_frame) - 2] = self._calculate_v5_frame_checksum(v5_frame) return v5_frame - def _v5_frame_decoder(self, v5_frame): + def _v5_frame_decoder(self, v5_frame) -> bytearray: """ Decodes a V5 data logging stick frame and returns a modbus RTU frame @@ -295,7 +300,7 @@ def _v5_frame_decoder(self, v5_frame): return modbus_frame - def _v5_time_response_frame(self, frame): + def _v5_time_response_frame(self, frame) -> bytearray: """ Creates time response frame @@ -304,14 +309,12 @@ def _v5_time_response_frame(self, frame): + struct.pack(" bytes: """ Send v5 frame to the data logger and receive response @@ -345,7 +348,7 @@ def _send_receive_v5_frame(self, data_logging_stick_frame): self.log.debug("[%s] RECD: %s", self.serial, v5_response.hex(" ")) return v5_response - def _received_frame_is_valid(self, frame): + def _received_frame_is_valid(self, frame) -> bool: """ Check that the frame is valid and that the serial number of the received frame matches with the last sent one @@ -359,7 +362,7 @@ def _received_frame_is_valid(self, frame): return False return True - def _received_frame_response(self, frame): + def _received_frame_response(self, frame) -> tuple[bool, bytearray]: """ Return response to frames with control codes 0x41 (handshake), 0x42 (data), 0x43 (wifi), 0x47 (heartbeat) and 0x48 (report) @@ -393,7 +396,7 @@ def _received_frame_response(self, frame): self.log.debug("[%s] V5_REPORT RESP: %s", self.serial, response_frame.hex(" ")) return do_continue, response_frame - def _handle_protocol_frame(self, frame): + def _handle_protocol_frame(self, frame) -> bool: """ Handles frames with known control codes :func:`_received_frame_response() ` From 4d120f737d60384b0ae125f6285973342961d010 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Thu, 26 Dec 2024 05:29:57 +0100 Subject: [PATCH 19/27] refactor: Turn _get_response_code into staticmethod --- pysolarmanv5/pysolarmanv5.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 43a9fec..28087e7 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -144,7 +144,15 @@ def __init__(self, address, serial, **kwargs): self._socket_setup(kwargs.get("socket"), kwargs.get("auto_reconnect", False)) @staticmethod - def _calculate_v5_frame_checksum(frame): + def _get_response_code(code) -> int: + """ + Get response control code from request control code + + """ + return code - 0x30 + + @staticmethod + def _calculate_v5_frame_checksum(frame) -> int: """ Calculate checksum on all frame bytes except head, end and checksum @@ -180,13 +188,6 @@ def _v5_trailer(self) -> bytearray: """ return bytearray(self.v5_checksum + self.v5_end) - def _get_response_code(self, control) -> int: - """ - Get response control code from request control code - - """ - return control - 0x30 - def _get_next_sequence_number(self) -> int: """ Get the next sequence number for use in outgoing packets From e2d14114c02092a43d91865a014ac962566b7812 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Thu, 26 Dec 2024 05:50:18 +0100 Subject: [PATCH 20/27] refactor: Generalize _received_frame_response --- pysolarmanv5/pysolarmanv5.py | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 28087e7..8f97dd8 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -122,6 +122,7 @@ def __init__(self, address, serial, **kwargs): self.v5_start = bytes.fromhex("A5") self.v5_length = bytes.fromhex("0000") # placeholder value self.v5_magic = bytes.fromhex("10") + self.v5_control_codes = CONTROL.__dict__.values() self.v5_serial = bytes.fromhex("0000") # placeholder value self.v5_loggerserial = struct.pack(" tuple[bool, bytearray]: """ do_continue = True response_frame = None - if frame[4] == CONTROL.HANDSHAKE: - do_continue = False - self.log.debug("[%s] V5_HANDSHAKE: %s", self.serial, frame.hex(" ")) + if frame[4] != CONTROL.REQUEST and frame[4] in self.v5_control_codes: + control_name = [i for i in CONTROL.__dict__ if CONTROL.__dict__[i]==frame[4]][0] + self.log.debug("[%s] V5_%s: %s", self.serial, control_name, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) - self.log.debug("[%s] V5_HANDSHAKE RESP: %s", self.serial, response_frame.hex(" ")) - if frame[4] == CONTROL.DATA: - do_continue = False # Maybe True and thus process the packet in the future? - self.log.debug("[%s] V5_DATA: %s", self.serial, frame.hex(" ")) - response_frame = self._v5_time_response_frame(frame) - self.log.debug("[%s] V5_DATA RESP: %s", self.serial, response_frame.hex(" ")) - if frame[4] == CONTROL.INFO: - do_continue = False - self.log.debug("[%s] V5_INFO: %s", self.serial, frame.hex(" ")) - response_frame = self._v5_time_response_frame(frame) - self.log.debug("[%s] V5_INFO RESP: %s", self.serial, response_frame.hex(" ")) - if frame[4] == CONTROL.HEARTBEAT: - do_continue = False - self.log.debug("[%s] V5_HEARTBEAT: %s", self.serial, frame.hex(" ")) - response_frame = self._v5_time_response_frame(frame) - self.log.debug("[%s] V5_HEARTBEAT RESP: %s", self.serial, response_frame.hex(" ")) - if frame[4] == CONTROL.REPORT: - do_continue = False - self.log.debug("[%s] V5_REPORT: %s", self.serial, frame.hex(" ")) - response_frame = self._v5_time_response_frame(frame) - self.log.debug("[%s] V5_REPORT RESP: %s", self.serial, response_frame.hex(" ")) + self.log.debug("[%s] V5_%s RESP: %s", self.serial, control_name, response_frame.hex(" ")) + # Maybe do_continue = True for CONTROL.DATA|INFO|REPORT and thus process packets in the future? return do_continue, response_frame def _handle_protocol_frame(self, frame) -> bool: From 5b19f625096d81edbd7610fa332334e9f28c5fbf Mon Sep 17 00:00:00 2001 From: David Rapan Date: Thu, 26 Dec 2024 05:58:23 +0100 Subject: [PATCH 21/27] refactor: Use of negative indexing --- pysolarmanv5/pysolarmanv5.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 8f97dd8..8fdf0b2 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -232,7 +232,7 @@ def _v5_frame_encoder(self, modbus_frame) -> bytearray: v5_frame = v5_header + v5_payload + self._v5_trailer() - v5_frame[len(v5_frame) - 2] = self._calculate_v5_frame_checksum(v5_frame) + v5_frame[-2] = self._calculate_v5_frame_checksum(v5_frame) return v5_frame def _v5_frame_decoder(self, v5_frame) -> bytearray: @@ -277,10 +277,10 @@ def _v5_frame_decoder(self, v5_frame) -> bytearray: frame_len = frame_len_without_payload_len + payload_len if (v5_frame[0] != int.from_bytes(self.v5_start, byteorder="big")) or ( - v5_frame[frame_len - 1] != int.from_bytes(self.v5_end, byteorder="big") + v5_frame[-1] != int.from_bytes(self.v5_end, byteorder="big") ): raise V5FrameError("V5 frame contains invalid start or end values") - if v5_frame[frame_len - 2] != self._calculate_v5_frame_checksum(v5_frame): + if v5_frame[-2] != self._calculate_v5_frame_checksum(v5_frame): raise V5FrameError("V5 frame contains invalid V5 checksum") if v5_frame[5] != self.sequence_number: raise V5FrameError("V5 frame contains invalid sequence number") @@ -291,7 +291,7 @@ def _v5_frame_decoder(self, v5_frame) -> bytearray: if v5_frame[11] != int("02", 16): raise V5FrameError("V5 frame contains invalid frametype") - modbus_frame = v5_frame[25 : frame_len - 2] + modbus_frame = v5_frame[25:-2] if len(modbus_frame) < 5: if len(modbus_frame) > 0 and ( From 5a0f55eb6d9f640ef9c27f9afa0eb273c6f6ff07 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Thu, 26 Dec 2024 06:08:28 +0100 Subject: [PATCH 22/27] refactor: serial, v5_serial and v5_loggerserial were hella confusing!!! --- pysolarmanv5/pysolarmanv5.py | 12 ++++++------ tests/setup_test.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 8fdf0b2..68eee5f 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -123,8 +123,8 @@ def __init__(self, address, serial, **kwargs): self.v5_length = bytes.fromhex("0000") # placeholder value self.v5_magic = bytes.fromhex("10") self.v5_control_codes = CONTROL.__dict__.values() - self.v5_serial = bytes.fromhex("0000") # placeholder value - self.v5_loggerserial = struct.pack(" bytearray: + self.v5_magic + struct.pack(" bytearray: @@ -217,9 +217,9 @@ def _v5_frame_encoder(self, modbus_frame) -> bytearray: """ length = 15 + len(modbus_frame) self.v5_length = struct.pack(" bytearray: raise V5FrameError("V5 frame contains invalid V5 checksum") if v5_frame[5] != self.sequence_number: raise V5FrameError("V5 frame contains invalid sequence number") - if v5_frame[7:11] != self.v5_loggerserial: + if v5_frame[7:11] != self.v5_serial: raise V5FrameError("V5 frame contains incorrect data logger serial number") if v5_frame[4] != self._get_response_code(CONTROL.REQUEST): raise V5FrameError("V5 frame contains incorrect control code") diff --git a/tests/setup_test.py b/tests/setup_test.py index ce8859c..673afd7 100644 --- a/tests/setup_test.py +++ b/tests/setup_test.py @@ -77,7 +77,7 @@ def v5_frame_response_encoder(self, modbus_frame): """ self.v5_length = struct.pack(" Date: Thu, 26 Dec 2024 06:38:57 +0100 Subject: [PATCH 23/27] refactor: MockDatalogger to reuse _v5_header and _v5_trailer "constructor" mothods --- pysolarmanv5/pysolarmanv5.py | 2 +- tests/setup_test.py | 21 +++++++-------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 68eee5f..8d2334f 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -216,6 +216,7 @@ def _v5_frame_encoder(self, modbus_frame) -> bytearray: """ length = 15 + len(modbus_frame) + self.v5_length = struct.pack(" bytearray: ) v5_frame = v5_header + v5_payload + self._v5_trailer() - v5_frame[-2] = self._calculate_v5_frame_checksum(v5_frame) return v5_frame diff --git a/tests/setup_test.py b/tests/setup_test.py index 673afd7..44b5b52 100644 --- a/tests/setup_test.py +++ b/tests/setup_test.py @@ -1,7 +1,7 @@ import socket import threading -from pysolarmanv5 import PySolarmanV5 +from pysolarmanv5 import CONTROL, PySolarmanV5 import struct from umodbus.client.serial.redundancy_check import add_crc from umodbus.functions import ( @@ -75,20 +75,14 @@ def v5_frame_response_encoder(self, modbus_frame): :rtype: bytearray """ + length = 14 + len(modbus_frame) - self.v5_length = struct.pack(" Date: Thu, 26 Dec 2024 07:15:44 +0100 Subject: [PATCH 24/27] feat: Update protocol docs with known control codes --- docs/solarmanv5_protocol.rst | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/solarmanv5_protocol.rst b/docs/solarmanv5_protocol.rst index 5acd81f..919d7d2 100644 --- a/docs/solarmanv5_protocol.rst +++ b/docs/solarmanv5_protocol.rst @@ -32,18 +32,26 @@ The Header is always 11 bytes and is composed of: * **Start** (*one byte*) – Denotes the start of the V5 frame. Always ``0xA5``. * **Length** (*two bytes*) – :ref:`Payload` length -* **Control Code** (*two bytes*) – Describes the type of V5 frame. - For Modbus RTU requests, the control code is ``0x4510``. - For Modbus RTU responses, the control code is ``0x1510``. -* **Serial** (*two bytes*) – This field acts as a two-way sequence number. On +* **Control Code** (*two bytes*) – Describes the type of V5 frame: + + * HANDSHAKE ``0x4110``, used for initial handshake in server mode + * DATA ``0x4210``, used for sending data in server mode + * INFO ``0x4310``, used for sending stick fw, ip and ssid info in server mode + * REQUEST ``0x4510``, for Modbus RTU requests in client mode + + * RESPONSE ``0x1510``, for Modbus RTU responses in client mode + * HEARTBEAT ``0x4710``, keepalive packets in both modes + * *REPORT* ``0x4810`` + *Responses are described as* ``request code - 0x3000`` *which can be seen in + Modbus RTU response - request pair:* ``0x4510 - 0x3000 = 0x1510`` +* **Sequence Number** (*two bytes*) – This field acts as a two-way sequence number. On outgoing requests, the first byte of this field is echoed back in the same position on incoming responses. pysolarmanv5 expoits this property to detect invalid responses. This is done by initialising this byte to a random value, and incrementing for each subsequent request. The second byte is incremented by the data logging stick for every response sent (either to Solarman Cloud or local requests). -* **Logger Serial** (*four bytes*) – Serial number of Solarman data logging - stick +* **Serial Number** (*four bytes*) – Serial number of data logging stick Payload ^^^^^^^ From ade1c3abb46c00d412abdfaf06c2b3d944237e7e Mon Sep 17 00:00:00 2001 From: David Rapan Date: Thu, 26 Dec 2024 07:33:44 +0100 Subject: [PATCH 25/27] feat: Add argument & return types --- pysolarmanv5/pysolarmanv5.py | 18 +++++++++--------- pysolarmanv5/pysolarmanv5_async.py | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index 8d2334f..a07a684 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -145,7 +145,7 @@ def __init__(self, address, serial, **kwargs): self._socket_setup(kwargs.get("socket"), kwargs.get("auto_reconnect", False)) @staticmethod - def _get_response_code(code) -> int: + def _get_response_code(code: int) -> int: """ Get response control code from request control code @@ -153,7 +153,7 @@ def _get_response_code(code) -> int: return code - 0x30 @staticmethod - def _calculate_v5_frame_checksum(frame) -> int: + def _calculate_v5_frame_checksum(frame: bytes) -> int: """ Calculate checksum on all frame bytes except head, end and checksum @@ -205,7 +205,7 @@ def _get_next_sequence_number(self) -> int: self.sequence_number = (self.sequence_number + 1) & 0xFF return self.sequence_number - def _v5_frame_encoder(self, modbus_frame) -> bytearray: + def _v5_frame_encoder(self, modbus_frame: bytes) -> bytearray: """ Take a modbus RTU frame and encode it in a V5 data logging stick frame @@ -235,7 +235,7 @@ def _v5_frame_encoder(self, modbus_frame) -> bytearray: v5_frame[-2] = self._calculate_v5_frame_checksum(v5_frame) return v5_frame - def _v5_frame_decoder(self, v5_frame) -> bytearray: + def _v5_frame_decoder(self, v5_frame: bytes) -> bytearray: """ Decodes a V5 data logging stick frame and returns a modbus RTU frame @@ -302,7 +302,7 @@ def _v5_frame_decoder(self, v5_frame) -> bytearray: return modbus_frame - def _v5_time_response_frame(self, frame) -> bytearray: + def _v5_time_response_frame(self, frame: bytes) -> bytearray: """ Creates time response frame @@ -316,7 +316,7 @@ def _v5_time_response_frame(self, frame) -> bytearray: response_frame[-2] = self._calculate_v5_frame_checksum(response_frame) return response_frame - def _send_receive_v5_frame(self, data_logging_stick_frame) -> bytes: + def _send_receive_v5_frame(self, data_logging_stick_frame: bytes) -> bytes: """ Send v5 frame to the data logger and receive response @@ -350,7 +350,7 @@ def _send_receive_v5_frame(self, data_logging_stick_frame) -> bytes: self.log.debug("[%s] RECD: %s", self.serial, v5_response.hex(" ")) return v5_response - def _received_frame_is_valid(self, frame) -> bool: + def _received_frame_is_valid(self, frame: bytes) -> bool: """ Check that the frame is valid and that the serial number of the received frame matches with the last sent one @@ -364,7 +364,7 @@ def _received_frame_is_valid(self, frame) -> bool: return False return True - def _received_frame_response(self, frame) -> tuple[bool, bytearray]: + def _received_frame_response(self, frame: bytes) -> tuple[bool, bytearray]: """ Return response to frames with control codes 0x41 (handshake), 0x42 (data), 0x43 (wifi), 0x47 (heartbeat) and 0x48 (report) @@ -379,7 +379,7 @@ def _received_frame_response(self, frame) -> tuple[bool, bytearray]: # Maybe do_continue = True for CONTROL.DATA|INFO|REPORT and thus process packets in the future? return do_continue, response_frame - def _handle_protocol_frame(self, frame) -> bool: + def _handle_protocol_frame(self, frame: bytes) -> bool: """ Handles frames with known control codes :func:`_received_frame_response() ` diff --git a/pysolarmanv5/pysolarmanv5_async.py b/pysolarmanv5/pysolarmanv5_async.py index 592f808..17c3761 100644 --- a/pysolarmanv5/pysolarmanv5_async.py +++ b/pysolarmanv5/pysolarmanv5_async.py @@ -139,7 +139,7 @@ async def disconnect(self) -> None: f"{e} can be during closing ignored." ) # pylint: disable=logging-fstring-interpolation - def _socket_setup(self, *args, **kwargs): + def _socket_setup(self, *args, **kwargs) -> None: """Socket setup method PySolarmanV5Async handles socket creation separately to base @@ -147,7 +147,7 @@ def _socket_setup(self, *args, **kwargs): """ - def _send_data(self, data: bytes): + def _send_data(self, data: bytes) -> None: """ Sends the data received from the socket to the receiver. @@ -161,7 +161,7 @@ def _send_data(self, data: bytes): self.data_queue.put_nowait(data) self.data_wanted_ev.clear() - async def _handle_protocol_frame(self, frame): + async def _handle_protocol_frame(self, frame: bytes) -> bool: """ Handles frames with known control codes :func:`_received_frame_response() ` """ From 652fbf8b0dee9c4503d31c27bd255693d38ac37f Mon Sep 17 00:00:00 2001 From: David Rapan Date: Thu, 26 Dec 2024 07:41:48 +0100 Subject: [PATCH 26/27] refactor: Use of PySolarmanV5._get_response_code in tests --- tests/setup_test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/setup_test.py b/tests/setup_test.py index 44b5b52..c8bc88f 100644 --- a/tests/setup_test.py +++ b/tests/setup_test.py @@ -120,7 +120,8 @@ def handle(self) -> None: self.sol.sequence_number = data[5] log.debug(f"[SrvHandler] RECD: {data}") data = bytearray(data) - data[3:5] = struct.pack(" Date: Sun, 29 Dec 2024 21:36:06 +0100 Subject: [PATCH 27/27] fix: do_continue = False --- pysolarmanv5/pysolarmanv5.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pysolarmanv5/pysolarmanv5.py b/pysolarmanv5/pysolarmanv5.py index a07a684..70c7db3 100644 --- a/pysolarmanv5/pysolarmanv5.py +++ b/pysolarmanv5/pysolarmanv5.py @@ -372,11 +372,12 @@ def _received_frame_response(self, frame: bytes) -> tuple[bool, bytearray]: do_continue = True response_frame = None if frame[4] != CONTROL.REQUEST and frame[4] in self.v5_control_codes: + do_continue = False + # Maybe do_continue = True for CONTROL.DATA|INFO|REPORT and thus process packets in the future? control_name = [i for i in CONTROL.__dict__ if CONTROL.__dict__[i]==frame[4]][0] self.log.debug("[%s] V5_%s: %s", self.serial, control_name, frame.hex(" ")) response_frame = self._v5_time_response_frame(frame) self.log.debug("[%s] V5_%s RESP: %s", self.serial, control_name, response_frame.hex(" ")) - # Maybe do_continue = True for CONTROL.DATA|INFO|REPORT and thus process packets in the future? return do_continue, response_frame def _handle_protocol_frame(self, frame: bytes) -> bool: