add Modbus polling mode for DCU1000 (#305)

* add Modbus scanning mode

* fix modbus polling for DCU 1000

* add modbus register for DCU 1000

* calculate meta values from modbus regs

* update changelog

* reduce code duplication

* refactor modbus_scan

* add additional unit tests
This commit is contained in:
Stefan Allius
2025-03-11 19:47:37 +01:00
committed by GitHub
parent be60f9ea1e
commit 88cb01f613
9 changed files with 386 additions and 32 deletions

View File

@@ -93,6 +93,11 @@ class Config():
Optional('forward', default=False): Use(bool),
},
Optional('modbus_polling', default=True): Use(bool),
Optional('modbus_scanning'): {
'start': Use(int),
Optional('step', default=0x400): Use(int),
Optional('bytes', default=0x10): Use(int),
},
Optional('suggested_area', default=""): Use(str),
Optional('sensor_list', default=0): Use(int),
Optional('pv1'): {
@@ -136,6 +141,11 @@ class Config():
Optional('forward', default=False): Use(bool),
},
Optional('modbus_polling', default=True): Use(bool),
Optional('modbus_scanning'): {
'start': Use(int),
Optional('step', default=0x400): Use(int),
Optional('bytes', default=0x10): Use(int),
},
Optional('suggested_area', default=""): Use(str),
Optional('sensor_list', default=0): Use(int),
Optional('pv1'): {

View File

@@ -98,13 +98,9 @@ class Talent(Message):
if serial_no in inverters:
inv = inverters[serial_no]
self.node_id = inv['node_id']
self.sug_area = inv['suggested_area']
self.modbus_polling = inv['modbus_polling']
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
self._set_config_parms(inv)
self.db.set_pv_module_details(inv)
if self.mb:
self.mb.set_node_id(self.node_id)
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
else:
self.node_id = ''
self.sug_area = ''
@@ -175,12 +171,17 @@ class Talent(Message):
def mb_timout_cb(self, exp_cnt):
self.mb_timer.start(self.mb_timeout)
if self.mb_scan:
self._send_modbus_scan()
return
if 2 == (exp_cnt % 30):
# logging.info("Regular Modbus Status request")
self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.DEBUG)
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS, 0x2000,
96, logging.DEBUG)
else:
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS, 0x3000,
48, logging.DEBUG)
def _init_new_client_conn(self) -> bool:
contact_name = self.contact_name
@@ -554,6 +555,9 @@ class Talent(Message):
logger.warning('Unknown Message')
self.inc_counter('Unknown_Msg')
return
if (self.mb_scan):
modbus_msg_len = self.data_len - hdr_len
self._dump_modbus_scan(data, hdr_len, modbus_msg_len)
for key, update, _ in self.mb.recv_resp(self.db, data[
hdr_len:]):

View File

@@ -257,21 +257,31 @@ class InfosG3P(Infos):
continue
info_id = row['reg']
result = Fmt.get_value(buf, addr, row)
yield from self.__update_val(node_id, info_id, result)
yield from self.__update_val(node_id, "GEN3PLUS", info_id, result)
yield from self.calc(sensor, node_id)
def calc(self, sensor: int = 0, node_id: str = '') \
-> Generator[tuple[str, bool], None, None]:
'''calculate meta values from the
stored values in Infos.db
sensor: sensor_list number
node_id: id-string for the node'''
reg_map = RegisterSel.get(sensor)
if 'calc' in reg_map:
for row in reg_map['calc'].values():
info_id = row['reg']
result = row['func'](self, row['params'])
yield from self.__update_val(node_id, info_id, result)
yield from self.__update_val(node_id, "CALC", info_id, result)
def __update_val(self, node_id, info_id, result):
def __update_val(self, node_id, source: str, info_id, result):
keys, level, unit, must_incr = self._key_obj(info_id)
if keys:
name, update = self.update_db(keys, must_incr, result)
yield keys[0], update
if update:
self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}'
self.tracer.log(level, f'[{node_id}] {source}: {name}'
f' : {result}{unit}')
def build(self, len, msg_type: int, rcv_ftype: int, sensor: int = 0):

View File

@@ -322,6 +322,8 @@ class SolarmanV5(SolarmanBase):
self.at_acl = g3p_cnf['at_acl']
self.sensor_list = 0
self.mb_regs = [{'addr': 0x3000, 'len': 48},
{'addr': 0x2000, 'len': 96}]
'''
Our puplic methods
@@ -357,7 +359,16 @@ class SolarmanV5(SolarmanBase):
self.new_data['controller'] = True
self.state = State.up
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
if self.mb_scan:
self._send_modbus_cmd(self.mb_inv_no, Modbus.READ_REGS,
self.mb_start_reg, self.mb_bytes,
logging.INFO)
else:
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS,
self.mb_regs[0]['addr'],
self.mb_regs[0]['len'], logging.DEBUG)
self.mb_timer.start(self.mb_timeout)
def new_state_up(self):
@@ -377,16 +388,16 @@ class SolarmanV5(SolarmanBase):
self.ifc.fwd_add(build_msg)
self.ifc.fwd_add(struct.pack('<BB', 0, 0x15)) # crc & stop
def __set_config_parms(self, inv: dict, serial_no: str):
def _set_config_parms(self, inv: dict, serial_no: str = ""):
'''init connection with params from the configuration'''
self.node_id = inv['node_id']
self.sug_area = inv['suggested_area']
self.modbus_polling = inv['modbus_polling']
super()._set_config_parms(inv)
self.sensor_list = inv['sensor_list']
if 0 == self.sensor_list:
snr = serial_no[:3]
if '410' == snr:
self.sensor_list = 0x3026
self.mb_regs = [{'addr': 0x0000, 'len': 45}]
else:
self.sensor_list = 0x02b0
self.db.set_db_def_value(Register.SENSOR_LIST,
@@ -394,9 +405,6 @@ class SolarmanV5(SolarmanBase):
logging.debug(f"Use sensor-list: {self.sensor_list:#04x}"
f" for '{serial_no}'")
if self.mb:
self.mb.set_node_id(self.node_id)
def _set_serial_no(self, snr: int):
'''check the serial number and configure the inverter connection'''
serial_no = str(snr)
@@ -411,7 +419,7 @@ class SolarmanV5(SolarmanBase):
# logger.debug(f'key: {key} -> {inv}')
if (type(inv) is dict and 'monitor_sn' in inv
and inv['monitor_sn'] == snr):
self.__set_config_parms(inv, key)
self._set_config_parms(inv, key)
self.db.set_pv_module_details(inv)
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
@@ -474,12 +482,18 @@ class SolarmanV5(SolarmanBase):
def mb_timout_cb(self, exp_cnt):
self.mb_timer.start(self.mb_timeout)
if self.mb_scan:
self._send_modbus_scan()
else:
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS,
self.mb_regs[0]['addr'],
self.mb_regs[0]['len'], logging.INFO)
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.INFO)
if 1 == (exp_cnt % 30):
# logging.info("Regular Modbus Status request")
self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.INFO)
if 1 == (exp_cnt % 30) and len(self.mb_regs) > 1:
# logging.info("Regular Modbus Status request")
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS,
self.mb_regs[1]['addr'],
self.mb_regs[1]['len'], logging.INFO)
def at_cmd_forbidden(self, cmd: str, connection: str) -> bool:
return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \
@@ -672,16 +686,25 @@ class SolarmanV5(SolarmanBase):
return
self.__forward_msg()
def __parse_modbus_rsp(self, data):
def __parse_modbus_rsp(self, data, modbus_msg_len):
inv_update = False
self.modbus_elms = 0
if (self.mb_scan):
self._dump_modbus_scan(data, 14, modbus_msg_len)
ts = self._timestamp()
for key, update, _ in self.mb.recv_resp(self.db, data[14:]):
self.modbus_elms += 1
if update:
if key == 'inverter':
inv_update = True
self._set_mqtt_timestamp(key, self._timestamp())
self._set_mqtt_timestamp(key, ts)
self.new_data[key] = True
for key, update in self.db.calc(self.sensor_list, self.node_id):
if update:
self._set_mqtt_timestamp(key, ts)
self.new_data[key] = True
return inv_update
def __modbus_command_rsp(self, data):
@@ -691,7 +714,7 @@ class SolarmanV5(SolarmanBase):
# logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}')
if valid == 1 and modbus_msg_len > 4:
# logger.info(f'first byte modbus:{data[14]}')
inv_update = self.__parse_modbus_rsp(data)
inv_update = self.__parse_modbus_rsp(data, modbus_msg_len)
if inv_update:
self.__build_model_name()

View File

@@ -117,6 +117,11 @@ class Message(ProtocolIfc):
self.mb_first_timeout = self.MB_START_TIMEOUT
'''timer value for next Modbus polling request'''
self.modbus_polling = False
self.mb_start_reg = 0
self.mb_step = 0
self.mb_bytes = 0
self.mb_inv_no = 1
self.mb_scan = False
@property
def node_id(self):
@@ -135,6 +140,25 @@ class Message(ProtocolIfc):
# to our _recv_buffer
return # pragma: no cover
def _set_config_parms(self, inv: dict):
'''init connection with params from the configuration'''
self.node_id = inv['node_id']
self.sug_area = inv['suggested_area']
self.modbus_polling = inv['modbus_polling']
if 'modbus_scanning' in inv:
scan = inv['modbus_scanning']
self.mb_scan = True
self.mb_start_reg = scan['start']
self.mb_step = scan['step']
self.mb_bytes = scan['bytes']
# if 'client_mode' in self.db and \
# self.db.client_mode:
self.mb_start_reg = scan['start']
# else:
# self.mb_start_reg = scan['start'] - scan['step']
if self.mb:
self.mb.set_node_id(self.node_id)
def _set_mqtt_timestamp(self, key, ts: float | None):
if key not in self.new_data or \
not self.new_data[key]:
@@ -160,15 +184,39 @@ class Message(ProtocolIfc):
to = self.MAX_DEF_IDLE_TIME
return to
def _send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
def _send_modbus_cmd(self, dev_id, func, addr, val, log_lvl) -> None:
if self.state != State.up:
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
' as the state is not UP')
return
self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl)
self.mb.build_msg(dev_id, func, addr, val, log_lvl)
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
self._send_modbus_cmd(func, addr, val, log_lvl)
self._send_modbus_cmd(Modbus.INV_ADDR, func, addr, val, log_lvl)
def _send_modbus_scan(self):
self.mb_start_reg += self.mb_step
if self.mb_start_reg > 0xffff:
self.mb_start_reg = self.mb_start_reg & 0xffff
self.mb_inv_no += 1
logging.info(f"Next Round: inv:{self.mb_inv_no}"
f" reg:{self.mb_start_reg:04x}")
if (self.mb_start_reg & 0xfffc) % 0x80 == 0:
logging.info(f"[{self.node_id}] Scan info: "
f"inv:{self.mb_inv_no}"
f" reg:{self.mb_start_reg:04x}")
self._send_modbus_cmd(self.mb_inv_no, Modbus.READ_REGS,
self.mb_start_reg, self.mb_bytes,
logging.INFO)
def _dump_modbus_scan(self, data, hdr_len, modbus_msg_len):
if (data[hdr_len] == self.mb_inv_no and
data[hdr_len+1] == Modbus.READ_REGS):
logging.info(f'[{self.node_id}] Valid MODBUS data '
f'(reg: 0x{self.mb.last_reg:04x}):')
hex_dump_memory(logging.INFO, 'Valid MODBUS data '
f'(reg: 0x{self.mb.last_reg:04x}):',
data[hdr_len:], modbus_msg_len)
'''
Our puplic methods

View File

@@ -37,6 +37,34 @@ class Modbus():
__crc_tab = []
mb_reg_mapping = {
0x0000: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
0x0008: {'reg': Register.BATT_PV1_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV1 voltage
0x0009: {'reg': Register.BATT_PV1_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV1 current
0x000a: {'reg': Register.BATT_PV2_VOLT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 voltage
0x000b: {'reg': Register.BATT_PV2_CUR, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, PV2 current
0x000c: {'reg': Register.BATT_38, 'fmt': '!h'}, # noqa: E501
0x000d: {'reg': Register.BATT_3a, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
0x000e: {'reg': Register.BATT_STATUS_1, 'fmt': '!h'}, # noqa: E501
0x000f: {'reg': Register.BATT_STATUS_2, 'fmt': '!h'}, # noqa: E501
0x0010: {'reg': Register.BATT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
0x0011: {'reg': Register.BATT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
0x0012: {'reg': Register.BATT_SOC, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501, state of charge (SOC) in percent
# 0x0013: {'reg': Register.BATT_46, 'fmt': '!h'}, # noqa: E501
# 0x0014: {'reg': Register.BATT_48, 'fmt': '!h'}, # noqa: E501
# 0x0015: {'reg': Register.BATT_4a, 'fmt': '!h'}, # noqa: E501
# 0x0016: {'reg': Register.BATT_4c, 'fmt': '!h'}, # noqa: E501
# 0x0017: {'reg': Register.BATT_4e, 'fmt': '!h'}, # noqa: E501
# 0x0023: {'reg': Register.BATT_66, 'fmt': '!h'}, # noqa: E501
# 0x0024: {'reg': Register.BATT_68, 'fmt': '!h'}, # noqa: E501
# 0x0025: {'reg': Register.BATT_6a, 'fmt': '!h'}, # noqa: E501
0x0026: {'reg': Register.BATT_OUT_VOLT, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
0x0027: {'reg': Register.BATT_OUT_CUR, 'fmt': '!h', 'ratio': 0.01}, # noqa: E501
# 0x0028: {'reg': Register.BATT_70, 'fmt': '!h'}, # noqa: E501
# 0x0029: {'reg': Register.BATT_72, 'fmt': '!h'}, # noqa: E501
# 0x002a: {'reg': Register.BATT_74, 'fmt': '!h'}, # noqa: E501
# 0x002b: {'reg': Register.BATT_76, 'fmt': '!h'}, # noqa: E501
# 0x002c: {'reg': Register.BATT_78, 'fmt': '!h'}, # noqa: E501
0x2000: {'reg': Register.BOOT_STATUS, 'fmt': '!H'}, # noqa: E501
0x2001: {'reg': Register.DSP_STATUS, 'fmt': '!H'}, # noqa: E501
0x2003: {'reg': Register.WORK_MODE, 'fmt': '!H'},