add initial DCU support
This commit is contained in:
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [unreleased]
|
## [unreleased]
|
||||||
|
|
||||||
|
- add initial DCU support
|
||||||
|
- update AddOn base docker image to version 17.1.2
|
||||||
|
- update aiohttp to version 3.11.12
|
||||||
- fix the path handling for logging.ini and default_config.toml [#180](https://github.com/s-allius/tsun-gen3-proxy/issues/180)
|
- fix the path handling for logging.ini and default_config.toml [#180](https://github.com/s-allius/tsun-gen3-proxy/issues/180)
|
||||||
|
|
||||||
## [0.12.1] - 2025-01-13
|
## [0.12.1] - 2025-01-13
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
from infos import Infos, Register, ProxyMode, Fmt
|
from infos import Infos, Register, ProxyMode, Fmt
|
||||||
|
|
||||||
@@ -32,7 +33,8 @@ class RegisterMap:
|
|||||||
0x4102008e: {'reg': None, 'fmt': '<B'}, # noqa: E501 Encryption Certificate File Status
|
0x4102008e: {'reg': None, 'fmt': '<B'}, # noqa: E501 Encryption Certificate File Status
|
||||||
0x4102008f: {'reg': None, 'fmt': '!40s'}, # noqa: E501
|
0x4102008f: {'reg': None, 'fmt': '!40s'}, # noqa: E501
|
||||||
0x410200b7: {'reg': Register.SSID, '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
|
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
|
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
|
0x42010020: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
|
||||||
@@ -110,6 +112,22 @@ class RegisterMap:
|
|||||||
0xffffff02: {'reg': Register.POLLING_INTERVAL},
|
0xffffff02: {'reg': Register.POLLING_INTERVAL},
|
||||||
# 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1}, # noqa: E501
|
# 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):
|
class InfosG3P(Infos):
|
||||||
@@ -144,7 +162,9 @@ class InfosG3P(Infos):
|
|||||||
entity strings
|
entity strings
|
||||||
sug_area:str ==> suggested area string from the config file'''
|
sug_area:str ==> suggested area string from the config file'''
|
||||||
# iterate over RegisterMap.map and get the register values
|
# 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']
|
info_id = row['reg']
|
||||||
if self.__hide_topic(row):
|
if self.__hide_topic(row):
|
||||||
res = self.ha_remove(info_id, node_id, snr) # noqa: E501
|
res = self.ha_remove(info_id, node_id, snr) # noqa: E501
|
||||||
@@ -153,13 +173,14 @@ class InfosG3P(Infos):
|
|||||||
if res:
|
if res:
|
||||||
yield 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]:
|
-> Generator[tuple[str, bool], None, None]:
|
||||||
'''parse a data sequence received from the inverter and
|
'''parse a data sequence received from the inverter and
|
||||||
stores the values in Infos.db
|
stores the values in Infos.db
|
||||||
|
|
||||||
buf: buffer of the sequence to parse'''
|
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
|
addr = idx & 0xffff
|
||||||
ftype = (idx >> 16) & 0xff
|
ftype = (idx >> 16) & 0xff
|
||||||
mtype = (idx >> 24) & 0xff
|
mtype = (idx >> 24) & 0xff
|
||||||
@@ -183,9 +204,9 @@ class InfosG3P(Infos):
|
|||||||
self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}'
|
self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}'
|
||||||
f' : {result}{unit}')
|
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)
|
buf = bytearray(len)
|
||||||
for idx, row in RegisterMap.map.items():
|
for idx, row in RegisterSel.get(sensor).items():
|
||||||
addr = idx & 0xffff
|
addr = idx & 0xffff
|
||||||
ftype = (idx >> 16) & 0xff
|
ftype = (idx >> 16) & 0xff
|
||||||
mtype = (idx >> 24) & 0xff
|
mtype = (idx >> 24) & 0xff
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class SolarmanEmu(SolarmanBase):
|
|||||||
self.data_timer.start(self.data_up_inv)
|
self.data_timer.start(self.data_up_inv)
|
||||||
_len = 420
|
_len = 420
|
||||||
ftype = 1
|
ftype = 1
|
||||||
build_msg = self.db.build(_len, 0x42, ftype)
|
build_msg = self.db.build(_len, 0x42, ftype, 0x02b0)
|
||||||
|
|
||||||
self._build_header(0x4210)
|
self._build_header(0x4210)
|
||||||
self.ifc.tx_add(
|
self.ifc.tx_add(
|
||||||
|
|||||||
@@ -516,11 +516,11 @@ class SolarmanV5(SolarmanBase):
|
|||||||
logger.info(f'Model: {model}')
|
logger.info(f'Model: {model}')
|
||||||
self.db.set_db_def_value(Register.EQUIPMENT_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
|
inv_update = False
|
||||||
msg_type = self.control >> 8
|
msg_type = self.control >> 8
|
||||||
for key, update in self.db.parse(self.ifc.rx_peek(), msg_type, ftype,
|
for key, update in self.db.parse(self.ifc.rx_peek(), msg_type,
|
||||||
self.node_id):
|
ftype, sensor, self.node_id):
|
||||||
if update:
|
if update:
|
||||||
if key == 'inverter':
|
if key == 'inverter':
|
||||||
inv_update = True
|
inv_update = True
|
||||||
@@ -581,7 +581,7 @@ class SolarmanV5(SolarmanBase):
|
|||||||
else:
|
else:
|
||||||
ts = None
|
ts = None
|
||||||
|
|
||||||
self.__process_data(ftype, ts)
|
self.__process_data(ftype, ts, sensor)
|
||||||
self.__forward_msg()
|
self.__forward_msg()
|
||||||
self.__send_ack_rsp(0x1210, ftype)
|
self.__send_ack_rsp(0x1210, ftype)
|
||||||
self.new_state_up()
|
self.new_state_up()
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ def test_parse_4210(inverter_data: bytes):
|
|||||||
i = InfosG3P(client_mode=False)
|
i = InfosG3P(client_mode=False)
|
||||||
i.db.clear()
|
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()
|
pass # side effect is calling generator i.parse()
|
||||||
|
|
||||||
assert json.dumps(i.db) == json.dumps({
|
assert json.dumps(i.db) == json.dumps({
|
||||||
@@ -127,10 +127,10 @@ def test_build_4210(inverter_data: bytes):
|
|||||||
i = InfosG3P(client_mode=False)
|
i = InfosG3P(client_mode=False)
|
||||||
i.db.clear()
|
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()
|
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):
|
for i in range(11, 31):
|
||||||
build_msg[i] = inverter_data[i]
|
build_msg[i] = inverter_data[i]
|
||||||
assert inverter_data == build_msg
|
assert inverter_data == build_msg
|
||||||
@@ -286,53 +286,53 @@ def test_build_ha_conf4():
|
|||||||
def test_exception_and_calc(inverter_data: bytes):
|
def test_exception_and_calc(inverter_data: bytes):
|
||||||
|
|
||||||
# patch table to convert temperature from °F to °C
|
# patch table to convert temperature from °F to °C
|
||||||
ofs = RegisterMap.map[0x420100d8]['offset']
|
ofs = RegisterMap.map_02b0[0x420100d8]['offset']
|
||||||
RegisterMap.map[0x420100d8]['quotient'] = 1.8
|
RegisterMap.map_02b0[0x420100d8]['quotient'] = 1.8
|
||||||
RegisterMap.map[0x420100d8]['offset'] = -32/1.8
|
RegisterMap.map_02b0[0x420100d8]['offset'] = -32/1.8
|
||||||
# map PV1_VOLTAGE to invalid register
|
# 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)
|
# set invalid maping entry for OUTPUT_POWER (string instead of dict type)
|
||||||
backup = RegisterMap.map[0x420100de]
|
backup = RegisterMap.map_02b0[0x420100de]
|
||||||
RegisterMap.map[0x420100de] = 'invalid_entry'
|
RegisterMap.map_02b0[0x420100de] = 'invalid_entry'
|
||||||
|
|
||||||
i = InfosG3P(client_mode=False)
|
i = InfosG3P(client_mode=False)
|
||||||
i.db.clear()
|
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()
|
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)
|
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[32:0xde] == inverter_data[32:0xde]
|
||||||
assert build_msg[0xde:0xe2] == b'\x00\x00\x00\x00'
|
assert build_msg[0xde:0xe2] == b'\x00\x00\x00\x00'
|
||||||
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
|
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
|
||||||
|
|
||||||
|
|
||||||
# remove a table entry and test parsing and building
|
# remove a table entry and test parsing and building
|
||||||
del RegisterMap.map[0x420100d8]['quotient']
|
del RegisterMap.map_02b0[0x420100d8]['quotient']
|
||||||
del RegisterMap.map[0x420100d8]['offset']
|
del RegisterMap.map_02b0[0x420100d8]['offset']
|
||||||
|
|
||||||
i.db.clear()
|
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()
|
pass # side effect is calling generator i.parse()
|
||||||
assert 54 == i.get_db_value(Register.INVERTER_TEMP, 0)
|
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[32:0xd8] == inverter_data[32:0xd8]
|
||||||
assert build_msg[0xd8:0xe2] == b'\x006\x00\x00\x02X\x00\x00\x00\x00'
|
assert build_msg[0xd8:0xe2] == b'\x006\x00\x00\x02X\x00\x00\x00\x00'
|
||||||
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
|
assert build_msg[0xe2:-1] == inverter_data[0xe2:-1]
|
||||||
|
|
||||||
# test restore table
|
# test restore table
|
||||||
RegisterMap.map[0x420100d8]['offset'] = ofs
|
RegisterMap.map_02b0[0x420100d8]['offset'] = ofs
|
||||||
RegisterMap.map[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping
|
RegisterMap.map_02b0[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping
|
||||||
RegisterMap.map[0x420100de] = backup # reset mapping
|
RegisterMap.map_02b0[0x420100de] = backup # reset mapping
|
||||||
|
|
||||||
# test orginial table
|
# test orginial table
|
||||||
i.db.clear()
|
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()
|
pass # side effect is calling generator i.parse()
|
||||||
assert 14 == i.get_db_value(Register.INVERTER_TEMP, 0)
|
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]
|
assert build_msg[32:-1] == inverter_data[32:-1]
|
||||||
|
|||||||
Reference in New Issue
Block a user