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
This commit is contained in:
Stefan Allius
2024-04-12 18:57:48 +02:00
parent 22f68ab330
commit 1d3a44c9f0
2 changed files with 151 additions and 83 deletions

View File

@@ -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('<BHHHL', buf, 0)
# store parsed header values in the class
start = result[0] # len of complete message
start = result[0] # start byte
self.data_len = result[1] # len of variable id string
self.control = result[2]
self.serial = result[3]
self.seq.set_recv(result[3])
self.snr = result[4]
if start != 0xA5:
@@ -194,6 +227,7 @@ class SolarmanV5(Message):
self._recv_buffer = bytearray()
return False
check = sum(buf[1:buf_len-2]) & 0xff
if check != crc:
self.inc_counter('Invalid_Msg_Format')
@@ -204,6 +238,36 @@ class SolarmanV5(Message):
return True
def __build_header(self, ctrl) -> None:
'''build header for new transmit message'''
self.send_msg_ofs = len(self._send_buffer)
self._send_buffer += struct.pack(
'<BHHHL', 0xA5, 0, ctrl, self.seq.get_send(), self.snr)
fnc = self.switch.get(ctrl, self.msg_unknown)
logger.info(self.__flow_str(self.server_side, 'tx') +
f' Ctl: {int(ctrl):#04x} Msg: {fnc.__name__!r}')
def __finish_send_msg(self) -> None:
'''finish the transmit message, set lenght and checksum'''
_len = len(self._send_buffer) - self.send_msg_ofs
struct.pack_into('<H', self._send_buffer, self.send_msg_ofs+1, _len-11)
check = sum(self._send_buffer[self.send_msg_ofs+1:self.send_msg_ofs +
_len]) & 0xff
self._send_buffer += struct.pack('<BB', check, 0x15) # crc & stop
def _update_header(self, _forward_buffer):
'''update header for message before forwarding,
set sequence and checksum'''
_len = len(_forward_buffer)
struct.pack_into('<H', _forward_buffer, 1,
_len-13)
struct.pack_into('<H', _forward_buffer, 5,
self.seq.get_send())
check = sum(_forward_buffer[1:_len-2]) & 0xff
struct.pack_into('<B', _forward_buffer, _len-2, check)
def __dispatch_msg(self) -> 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('<H', frame[-2:])
if calc_crc == frame_crc:
return 1
else:
return 0
'''
'''
Message handler methods
'''
@@ -280,6 +310,11 @@ class SolarmanV5(Message):
self.__process_data(ftype)
self.forward(self._recv_buffer, self.header_len+self.data_len+2)
self.__build_header(0x1110)
self._send_buffer += struct.pack('<BBLL', ftype, 1,
self._timestamp(),
self._heartbeat())
self.__finish_send_msg()
def msg_dev_rsp(self):
self.msg_response()
@@ -299,9 +334,13 @@ class SolarmanV5(Message):
dt = datetime.fromtimestamp(total)
logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
ftype &= 0x7f # mask bit 7 (0x80)
self.__process_data(ftype)
self.__process_data(ftype & 0x7f) # mask bit 7 (0x80)
self.forward(self._recv_buffer, self.header_len+self.data_len+2)
self.__build_header(0x1210)
self._send_buffer += struct.pack('<BBLL', ftype, 1,
self._timestamp(),
self._heartbeat())
self.__finish_send_msg()
def __process_data(self, ftype):
inv_update = False
@@ -343,6 +382,11 @@ class SolarmanV5(Message):
def msg_hbeat_ind(self):
self.forward(self._recv_buffer, self.header_len+self.data_len+2)
self.__build_header(0x1710)
self._send_buffer += struct.pack('<BBLL', 0, 1,
self._timestamp(),
self._heartbeat())
self.__finish_send_msg()
def msg_hbeat_rsp(self):
self.msg_response()
@@ -359,7 +403,6 @@ class SolarmanV5(Message):
dt = datetime.fromtimestamp(ts)
logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
self.forward(self._recv_buffer, self.header_len+self.data_len+2)
def at_command_ind(self):
self.inc_counter('AT_Command')

View File

@@ -1,4 +1,7 @@
import pytest, json
import pytest
import struct
import time
from datetime import datetime
from app.src.gen3plus.solarman_v5 import SolarmanV5
from app.src.config import Config
from app.src.infos import Infos, Register
@@ -6,6 +9,9 @@ from app.src.infos import Infos, Register
# initialize the proxy statistics
Infos.static_init()
timestamp = int(time.time()) # 1712861197
heartbeat = 60
class MemoryStream(SolarmanV5):
def __init__(self, msg, chunks = (0,), server_side: bool = True):
super().__init__(server_side)
@@ -19,6 +25,12 @@ class MemoryStream(SolarmanV5):
self.db.stat['proxy']['Invalid_Msg_Format'] = 0
self.db.stat['proxy']['AT_Command'] = 0
def _timestamp(self):
return timestamp
def _heartbeat(self) -> 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('<L',ts)
def hb():
hb = heartbeat
# convert int to little-endian bytes
return struct.pack('<L',hb)
def correct_checksum(buf):
checksum = sum(buf[1:]) & 0xff
return checksum.to_bytes(length=1)
@@ -90,8 +109,9 @@ def DeviceIndMsg(): # 0x4110
@pytest.fixture
def DeviceRspMsg(): # 0x1110
msg = b'\xa5\x0a\x00\x10\x11\x10\x84' +get_sn() +b'\x01\x01\x69\x6f\x09'
msg += b'\x66\x78\x00\x00\x00'
msg = b'\xa5\x0a\x00\x10\x11\x01\x01' +get_sn() +b'\x02\x01'
msg += total()
msg += hb()
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@@ -158,7 +178,7 @@ def InvalidChecksum(): # 0x4110
@pytest.fixture
def InverterIndMsg(): # 0x4210
msg = b'\xa5\x99\x01\x10\x42\xe6\x9e' +get_sn() +b'\x01\xb0\x02\xbc\xc8'
msg = b'\xa5\x99\x01\x10\x42\x01\x02' +get_sn() +b'\x01\xb0\x02\xbc\xc8'
msg += b'\x24\x32\x6c\x1f\x00\x00\xa0\x47\xe4\x33\x01\x00\x03\x08\x00\x00'
msg += b'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
@@ -290,8 +310,9 @@ def InverterIndMsg2000(): # 0x4210 rated Power 2000W
@pytest.fixture
def InverterRspMsg(): # 0x1210
msg = b'\xa5\x0a\x00\x10\x12\x10\x84' +get_sn() +b'\x01\x01\x69\x6f\x09'
msg += b'\x66\x78\x00\x00\x00'
msg = b'\xa5\x0a\x00\x10\x12\x02\02' +get_sn() +b'\x01\x01'
msg += total()
msg += hb()
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@@ -314,8 +335,9 @@ def HeartbeatIndMsg(): # 0x4710
@pytest.fixture
def HeartbeatRspMsg(): # 0x1710
msg = b'\xa5\x0a\x00\x10\x17\x10\x84' +get_sn() +b'\x00\x01\x22\x71\x09'
msg += b'\x66\x78\x00\x00\x00'
msg = b'\xa5\x0a\x00\x10\x17\x11\x84' +get_sn() +b'\x00\x01'
msg += total()
msg += hb()
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@@ -349,7 +371,7 @@ def test_read_message(DeviceIndMsg):
assert m.snr == 2070233889
assert m.unique_id == None
assert m.control == 0x4110
assert m.serial == 0x0100
assert str(m.seq) == '01:00'
assert m.data_len == 0xd4
assert m._recv_buffer==b''
assert m._send_buffer==b''
@@ -370,7 +392,7 @@ def test_invalid_start_byte(InvalidStartByte, DeviceIndMsg):
assert m.snr == 2070233889
assert m.unique_id == 0
assert m.control == 0x4110
assert m.serial == 0x0100
assert str(m.seq) == '01:00'
assert m.data_len == 0xd4
assert m._recv_buffer==b''
assert m._send_buffer==b''
@@ -390,7 +412,7 @@ def test_invalid_stop_byte(InvalidStopByte):
assert m.snr == 2070233889
assert m.unique_id == 0
assert m.control == 0x4110
assert m.serial == 0x0100
assert str(m.seq) == '01:00'
assert m.data_len == 0xd4
assert m._recv_buffer==b''
assert m._send_buffer==b''
@@ -410,7 +432,7 @@ def test_invalid_stop_byte2(InvalidStopByte, DeviceIndMsg):
assert m.snr == 2070233889
assert m.unique_id == 0
assert m.control == 0x4110
assert m.serial == 0x0100
assert str(m.seq) == '01:00'
assert m.data_len == 0xd4
assert m._recv_buffer==DeviceIndMsg
assert m._send_buffer==b''
@@ -424,7 +446,7 @@ def test_invalid_stop_byte2(InvalidStopByte, DeviceIndMsg):
assert m.snr == 2070233889
assert m.unique_id == None
assert m.control == 0x4110
assert m.serial == 0x0100
assert str(m.seq) == '01:00'
assert m.data_len == 0xd4
assert m._recv_buffer==b''
assert m._send_buffer==b''
@@ -446,7 +468,7 @@ def test_invalid_stop_start_byte(InvalidStopByte, InvalidStartByte):
assert m.snr == 2070233889
assert m.unique_id == 0
assert m.control == 0x4110
assert m.serial == 0x0100
assert str(m.seq) == '01:00'
assert m.data_len == 0xd4
assert m._recv_buffer==b''
assert m._send_buffer==b''
@@ -466,7 +488,7 @@ def test_invalid_checksum(InvalidChecksum, DeviceIndMsg):
assert m.snr == 2070233889
assert m.unique_id == 0
assert m.control == 0x4110
assert m.serial == 0x0100
assert str(m.seq) == '01:00'
assert m.data_len == 0xd4
assert m._recv_buffer==DeviceIndMsg
assert m._send_buffer==b''
@@ -480,7 +502,7 @@ def test_invalid_checksum(InvalidChecksum, DeviceIndMsg):
assert m.snr == 2070233889
assert m.unique_id == None
assert m.control == 0x4110
assert m.serial == 0x0100
assert str(m.seq) == '01:00'
assert m.data_len == 0xd4
assert m._recv_buffer==b''
assert m._send_buffer==b''
@@ -488,7 +510,7 @@ def test_invalid_checksum(InvalidChecksum, DeviceIndMsg):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
m.close()
def test_read_message_twice(ConfigNoTsunInv1, DeviceIndMsg):
def test_read_message_twice(ConfigNoTsunInv1, DeviceIndMsg, DeviceRspMsg):
ConfigNoTsunInv1
m = MemoryStream(DeviceIndMsg, (0,))
m.append_msg(DeviceIndMsg)
@@ -499,10 +521,13 @@ def test_read_message_twice(ConfigNoTsunInv1, DeviceIndMsg):
assert m.snr == 2070233889
assert m.unique_id == '2070233889'
assert m.control == 0x4110
assert m.serial == 0x0100
assert str(m.seq) == '01:01'
assert m.data_len == 0xd4
assert m._send_buffer==DeviceRspMsg
assert m._forward_buffer==b''
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m._send_buffer = bytearray(0) # clear send buffer for next test
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 2
@@ -510,8 +535,9 @@ def test_read_message_twice(ConfigNoTsunInv1, DeviceIndMsg):
assert m.snr == 2070233889
assert m.unique_id == '2070233889'
assert m.control == 0x4110
assert m.serial == 0x0100
assert str(m.seq) == '01:01'
assert m.data_len == 0xd4
assert m._send_buffer==DeviceRspMsg
assert m._forward_buffer==b''
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
@@ -528,7 +554,7 @@ def test_read_message_in_chunks(DeviceIndMsg):
assert m.snr == 2070233889
assert m.unique_id == 0 # should be None ?
assert m.control == 0x4110
assert m.serial == 0x0100
assert str(m.seq) == '01:00'
assert m.data_len == 0xd4
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.read() # read rest of message
@@ -552,7 +578,7 @@ def test_read_message_in_chunks2(ConfigTsunInv1, DeviceIndMsg):
assert m.snr == 2070233889
assert m.unique_id == '2070233889'
assert m.control == 0x4110
assert m.serial == 0x0100
assert str(m.seq) == '01:01'
assert m.data_len == 0xd4
assert m.msg_count == 1
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
@@ -563,7 +589,7 @@ def test_read_message_in_chunks2(ConfigTsunInv1, DeviceIndMsg):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
def test_read_two_messages(ConfigTsunAllowAll, DeviceIndMsg, InverterIndMsg):
def test_read_two_messages(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, InverterIndMsg, InverterRspMsg):
ConfigTsunAllowAll
m = MemoryStream(DeviceIndMsg, (0,))
m.append_msg(InverterIndMsg)
@@ -574,14 +600,13 @@ def test_read_two_messages(ConfigTsunAllowAll, DeviceIndMsg, InverterIndMsg):
assert m.snr == 2070233889
assert m.unique_id == '2070233889'
assert m.control == 0x4110
assert m.serial == 0x0100
assert str(m.seq) == '01:01'
assert m.data_len == 0xd4
assert m.msg_count == 1
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
assert m._forward_buffer==DeviceIndMsg
assert m._send_buffer==b''
# assert m._send_buffer==MsgContactResp
assert m._send_buffer==DeviceRspMsg
m._send_buffer = bytearray(0) # clear send buffer for next test
m._init_new_client_conn()
assert m._send_buffer==b''
@@ -597,11 +622,11 @@ def test_read_two_messages(ConfigTsunAllowAll, DeviceIndMsg, InverterIndMsg):
assert m.snr == 2070233889
assert m.unique_id == '2070233889'
assert m.control == 0x4210
assert m.serial == 0x9ee6
assert str(m.seq) == '02:02'
assert m.data_len == 0x199
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
assert m._forward_buffer==InverterIndMsg
assert m._send_buffer==b''
assert m._send_buffer==InverterRspMsg
m._send_buffer = bytearray(0) # clear send buffer for next test
m._init_new_client_conn()
@@ -618,7 +643,7 @@ def test_unkown_message(ConfigTsunInv1, UnknownMsg):
assert m.snr == 2070233889
assert m.unique_id == '2070233889'
assert m.control == 0x5110
assert m.serial == 0x8410
assert str(m.seq) == '84:10'
assert m.data_len == 0x0a
assert m._recv_buffer==b''
assert m._send_buffer==b''
@@ -636,11 +661,11 @@ def test_device_rsp(ConfigTsunInv1, DeviceRspMsg):
assert m.snr == 2070233889
assert m.unique_id == '2070233889'
assert m.control == 0x1110
assert m.serial == 0x8410
assert str(m.seq) == '01:01'
assert m.data_len == 0x0a
assert m._recv_buffer==b''
assert m._send_buffer==b''
assert m._forward_buffer==DeviceRspMsg
assert m._forward_buffer==b'' # DeviceRspMsg
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
@@ -654,15 +679,15 @@ def test_inverter_rsp(ConfigTsunInv1, InverterRspMsg):
assert m.snr == 2070233889
assert m.unique_id == '2070233889'
assert m.control == 0x1210
assert m.serial == 0x8410
assert str(m.seq) == '02:02'
assert m.data_len == 0x0a
assert m._recv_buffer==b''
assert m._send_buffer==b''
assert m._forward_buffer==InverterRspMsg
assert m._forward_buffer==b'' # InverterRspMsg
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
def test_heartbeat_ind(ConfigTsunInv1, HeartbeatIndMsg):
def test_heartbeat_ind(ConfigTsunInv1, HeartbeatIndMsg, HeartbeatRspMsg):
ConfigTsunInv1
m = MemoryStream(HeartbeatIndMsg, (0,))
m.read() # read complete msg, and dispatch msg
@@ -672,10 +697,10 @@ def test_heartbeat_ind(ConfigTsunInv1, HeartbeatIndMsg):
assert m.snr == 2070233889
# assert m.unique_id == '2070233889'
assert m.control == 0x4710
assert m.serial == 0x8410
assert str(m.seq) == '84:11' # value after sending response
assert m.data_len == 0x01
assert m._recv_buffer==b''
assert m._send_buffer==b''
assert m._send_buffer==HeartbeatRspMsg
assert m._forward_buffer==HeartbeatIndMsg
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
@@ -690,11 +715,11 @@ def test_heartbeat_rsp(ConfigTsunInv1, HeartbeatRspMsg):
assert m.snr == 2070233889
assert m.unique_id == '2070233889'
assert m.control == 0x1710
assert m.serial == 0x8410
assert str(m.seq) == '11:84' # value after sending response
assert m.data_len == 0x0a
assert m._recv_buffer==b''
assert m._send_buffer==b''
assert m._forward_buffer==HeartbeatRspMsg
assert m._forward_buffer==b'' # HeartbeatRspMsg
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
@@ -708,7 +733,7 @@ def test_at_command_ind(ConfigTsunInv1, AtCommandIndMsg):
assert m.snr == 2070233889
# assert m.unique_id == '2070233889'
assert m.control == 0x4510
assert m.serial == 0x8410
assert str(m.seq) == '84:10'
assert m.data_len == 0x01
assert m._recv_buffer==b''
assert m._send_buffer==b''