add initial DCU support

This commit is contained in:
Stefan Allius
2025-02-11 00:08:57 +01:00
parent cfdd65606d
commit 42fe33bacf
5 changed files with 55 additions and 31 deletions

View File

@@ -1,5 +1,6 @@
from typing import Generator
from itertools import chain
from infos import Infos, Register, ProxyMode, Fmt
@@ -32,7 +33,8 @@ class RegisterMap:
0x4102008e: {'reg': None, 'fmt': '<B'}, # noqa: E501 Encryption Certificate File Status
0x4102008f: {'reg': None, 'fmt': '!40s'}, # noqa: E501
0x410200b7: {'reg': Register.SSID, 'fmt': '!40s'}, # noqa: E501
}
map_02b0 = {
0x4201000c: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'func': Fmt.hex4}, # noqa: E501
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501, or packet number
0x42010020: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
@@ -110,6 +112,22 @@ class RegisterMap:
0xffffff02: {'reg': Register.POLLING_INTERVAL},
# 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
}
map_3026 = {
0x4201000c: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'func': Fmt.hex4}, # noqa: E501
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501, or packet number
0x42010020: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
}
class RegisterSel:
__sensor_map = {
0x02b0: RegisterMap.map_02b0,
0x3026: RegisterMap.map_3026,
}
@classmethod
def get(cls, sensor: int):
return cls.__sensor_map.get(sensor, RegisterMap.map)
class InfosG3P(Infos):
@@ -144,7 +162,9 @@ class InfosG3P(Infos):
entity strings
sug_area:str ==> suggested area string from the config file'''
# iterate over RegisterMap.map and get the register values
for row in RegisterMap.map.values():
for _, row in chain(RegisterMap.map.items(),
RegisterMap.map_02b0.items(),
RegisterMap.map_3026.items()):
info_id = row['reg']
if self.__hide_topic(row):
res = self.ha_remove(info_id, node_id, snr) # noqa: E501
@@ -153,13 +173,14 @@ class InfosG3P(Infos):
if res:
yield res
def parse(self, buf, msg_type: int, rcv_ftype: int, node_id: str = '') \
def parse(self, buf, msg_type: int, rcv_ftype: int,
sensor: int = 0, node_id: str = '') \
-> Generator[tuple[str, bool], None, None]:
'''parse a data sequence received from the inverter and
stores the values in Infos.db
buf: buffer of the sequence to parse'''
for idx, row in RegisterMap.map.items():
for idx, row in RegisterSel.get(sensor).items():
addr = idx & 0xffff
ftype = (idx >> 16) & 0xff
mtype = (idx >> 24) & 0xff
@@ -183,9 +204,9 @@ class InfosG3P(Infos):
self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}'
f' : {result}{unit}')
def build(self, len, msg_type: int, rcv_ftype: int):
def build(self, len, msg_type: int, rcv_ftype: int, sensor: int = 0):
buf = bytearray(len)
for idx, row in RegisterMap.map.items():
for idx, row in RegisterSel.get(sensor).items():
addr = idx & 0xffff
ftype = (idx >> 16) & 0xff
mtype = (idx >> 24) & 0xff

View File

@@ -103,7 +103,7 @@ class SolarmanEmu(SolarmanBase):
self.data_timer.start(self.data_up_inv)
_len = 420
ftype = 1
build_msg = self.db.build(_len, 0x42, ftype)
build_msg = self.db.build(_len, 0x42, ftype, 0x02b0)
self._build_header(0x4210)
self.ifc.tx_add(

View File

@@ -516,11 +516,11 @@ class SolarmanV5(SolarmanBase):
logger.info(f'Model: {model}')
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, model)
def __process_data(self, ftype, ts):
def __process_data(self, ftype, ts, sensor=0):
inv_update = False
msg_type = self.control >> 8
for key, update in self.db.parse(self.ifc.rx_peek(), msg_type, ftype,
self.node_id):
for key, update in self.db.parse(self.ifc.rx_peek(), msg_type,
ftype, sensor, self.node_id):
if update:
if key == 'inverter':
inv_update = True
@@ -581,7 +581,7 @@ class SolarmanV5(SolarmanBase):
else:
ts = None
self.__process_data(ftype, ts)
self.__process_data(ftype, ts, sensor)
self.__forward_msg()
self.__send_ack_rsp(0x1210, ftype)
self.new_state_up()

View File

@@ -105,7 +105,7 @@ def test_parse_4210(inverter_data: bytes):
i = InfosG3P(client_mode=False)
i.db.clear()
for key, update in i.parse (inverter_data, 0x42, 1):
for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0):
pass # side effect is calling generator i.parse()
assert json.dumps(i.db) == json.dumps({
@@ -127,10 +127,10 @@ def test_build_4210(inverter_data: bytes):
i = InfosG3P(client_mode=False)
i.db.clear()
for key, update in i.parse (inverter_data, 0x42, 1):
for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0):
pass # side effect is calling generator i.parse()
build_msg = i.build(len(inverter_data), 0x42, 1)
build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0)
for i in range(11, 31):
build_msg[i] = inverter_data[i]
assert inverter_data == build_msg
@@ -286,53 +286,53 @@ def test_build_ha_conf4():
def test_exception_and_calc(inverter_data: bytes):
# patch table to convert temperature from °F to °C
ofs = RegisterMap.map[0x420100d8]['offset']
RegisterMap.map[0x420100d8]['quotient'] = 1.8
RegisterMap.map[0x420100d8]['offset'] = -32/1.8
ofs = RegisterMap.map_02b0[0x420100d8]['offset']
RegisterMap.map_02b0[0x420100d8]['quotient'] = 1.8
RegisterMap.map_02b0[0x420100d8]['offset'] = -32/1.8
# map PV1_VOLTAGE to invalid register
RegisterMap.map[0x420100e0]['reg'] = Register.TEST_REG2
RegisterMap.map_02b0[0x420100e0]['reg'] = Register.TEST_REG2
# set invalid maping entry for OUTPUT_POWER (string instead of dict type)
backup = RegisterMap.map[0x420100de]
RegisterMap.map[0x420100de] = 'invalid_entry'
backup = RegisterMap.map_02b0[0x420100de]
RegisterMap.map_02b0[0x420100de] = 'invalid_entry'
i = InfosG3P(client_mode=False)
i.db.clear()
for key, update in i.parse (inverter_data, 0x42, 1):
for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0):
pass # side effect is calling generator i.parse()
assert math.isclose(12.2222, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09)
build_msg = i.build(len(inverter_data), 0x42, 1)
build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0)
assert build_msg[32:0xde] == inverter_data[32:0xde]
assert build_msg[0xde:0xe2] == b'\x00\x00\x00\x00'
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
# remove a table entry and test parsing and building
del RegisterMap.map[0x420100d8]['quotient']
del RegisterMap.map[0x420100d8]['offset']
del RegisterMap.map_02b0[0x420100d8]['quotient']
del RegisterMap.map_02b0[0x420100d8]['offset']
i.db.clear()
for key, update in i.parse (inverter_data, 0x42, 1):
for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0):
pass # side effect is calling generator i.parse()
assert 54 == i.get_db_value(Register.INVERTER_TEMP, 0)
build_msg = i.build(len(inverter_data), 0x42, 1)
build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0)
assert build_msg[32:0xd8] == inverter_data[32:0xd8]
assert build_msg[0xd8:0xe2] == b'\x006\x00\x00\x02X\x00\x00\x00\x00'
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
# test restore table
RegisterMap.map[0x420100d8]['offset'] = ofs
RegisterMap.map[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping
RegisterMap.map[0x420100de] = backup # reset mapping
RegisterMap.map_02b0[0x420100d8]['offset'] = ofs
RegisterMap.map_02b0[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping
RegisterMap.map_02b0[0x420100de] = backup # reset mapping
# test orginial table
i.db.clear()
for key, update in i.parse (inverter_data, 0x42, 1):
for key, update in i.parse (inverter_data, 0x42, 1, 0x02b0):
pass # side effect is calling generator i.parse()
assert 14 == i.get_db_value(Register.INVERTER_TEMP, 0)
build_msg = i.build(len(inverter_data), 0x42, 1)
build_msg = i.build(len(inverter_data), 0x42, 1, 0x02b0)
assert build_msg[32:-1] == inverter_data[32:-1]