add dcu_power MQTT topic (#416)
* add dcu_power MQTT topic * add DCU_COMMAND counter * test invalid dcu_power values * handle and test DCU Command responses * test dcu commands from the TSUN cloud * cleanup MQTT topic handling * update changelog * test MQTT error and exception handling * increase test coverage * test dispatcher exceptions * fix full_topic definition in dispatch test
This commit is contained in:
39
app/src/gen3plus/solarman_v5.py
Normal file → Executable file
39
app/src/gen3plus/solarman_v5.py
Normal file → Executable file
@@ -247,6 +247,7 @@ class SolarmanBase(Message):
|
||||
class SolarmanV5(SolarmanBase):
|
||||
AT_CMD = 1
|
||||
MB_RTU_CMD = 2
|
||||
DCU_CMD = 5
|
||||
AT_CMD_RSP = 8
|
||||
MB_CLIENT_DATA_UP = 30
|
||||
'''Data up time in client mode'''
|
||||
@@ -532,6 +533,26 @@ class SolarmanV5(SolarmanBase):
|
||||
except Exception:
|
||||
self.ifc.tx_clear()
|
||||
|
||||
def send_dcu_cmd(self, pdu: bytearray):
|
||||
if self.sensor_list != 0x3026:
|
||||
logger.debug(f'[{self.node_id}] DCU CMD not allowed,'
|
||||
f' for sensor: {self.sensor_list:#04x}')
|
||||
return
|
||||
|
||||
if self.state != State.up:
|
||||
logger.warning(f'[{self.node_id}] ignore DCU CMD,'
|
||||
' cause the state is not UP anymore')
|
||||
return
|
||||
|
||||
self.inverter.forward_dcu_cmd_resp = False
|
||||
self._build_header(0x4510)
|
||||
self.ifc.tx_add(struct.pack('<BHLLL', self.DCU_CMD,
|
||||
self.sensor_list, 0, 0, 0))
|
||||
self.ifc.tx_add(pdu)
|
||||
self._finish_send_msg()
|
||||
self.ifc.tx_log(logging.INFO, f'Send DCU CMD :{self.addr}:')
|
||||
self.ifc.tx_flush()
|
||||
|
||||
def __forward_msg(self):
|
||||
self.forward(self.ifc.rx_peek(), self.header_len+self.data_len+2)
|
||||
|
||||
@@ -647,6 +668,10 @@ class SolarmanV5(SolarmanBase):
|
||||
self.inc_counter('AT_Command')
|
||||
self.inverter.forward_at_cmd_resp = True
|
||||
|
||||
if ftype == self.DCU_CMD:
|
||||
self.inc_counter('DCU_Command')
|
||||
self.inverter.forward_dcu_cmd_resp = True
|
||||
|
||||
elif ftype == self.MB_RTU_CMD:
|
||||
rstream = self.ifc.remote.stream
|
||||
if rstream.mb.recv_req(data[15:],
|
||||
@@ -670,6 +695,10 @@ class SolarmanV5(SolarmanBase):
|
||||
if self.inverter.forward_at_cmd_resp:
|
||||
return logging.INFO
|
||||
return logging.DEBUG
|
||||
elif ftype == self.DCU_CMD:
|
||||
if self.inverter.forward_dcu_cmd_resp:
|
||||
return logging.INFO
|
||||
return logging.DEBUG
|
||||
elif ftype == self.MB_RTU_CMD \
|
||||
and self.server_side:
|
||||
return self.mb.last_log_lvl
|
||||
@@ -689,6 +718,16 @@ class SolarmanV5(SolarmanBase):
|
||||
logger.info(f'{key}: {data_json}')
|
||||
self.publish_mqtt(f'{Proxy.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||
return
|
||||
|
||||
elif ftype == self.DCU_CMD:
|
||||
if not self.inverter.forward_dcu_cmd_resp:
|
||||
data_json = '+ok'
|
||||
node_id = self.node_id
|
||||
key = 'dcu_resp'
|
||||
logger.info(f'{key}: {data_json}')
|
||||
self.publish_mqtt(f'{Proxy.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||
return
|
||||
|
||||
elif ftype == self.MB_RTU_CMD:
|
||||
self.__modbus_command_rsp(data)
|
||||
return
|
||||
|
||||
@@ -44,6 +44,7 @@ class Register(Enum):
|
||||
MODBUS_COMMAND = 60
|
||||
AT_COMMAND_BLOCKED = 61
|
||||
CLOUD_CONN_CNT = 62
|
||||
DCU_COMMAND = 63
|
||||
OUTPUT_POWER = 83
|
||||
RATED_POWER = 84
|
||||
INVERTER_TEMP = 85
|
||||
@@ -625,6 +626,7 @@ class Infos:
|
||||
Register.INVALID_MSG_FMT: {'name': ['proxy', 'Invalid_Msg_Format'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_msg_fmt_', 'fmt': FMT_INT, 'name': 'Invalid Message Format', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.AT_COMMAND: {'name': ['proxy', 'AT_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_', 'fmt': FMT_INT, 'name': 'AT Command', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.AT_COMMAND_BLOCKED: {'name': ['proxy', 'AT_Command_Blocked'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_blocked_', 'fmt': FMT_INT, 'name': 'AT Command Blocked', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.DCU_COMMAND: {'name': ['proxy', 'DCU_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'dcu_cmd_', 'fmt': FMT_INT, 'name': 'DCU Command', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.MODBUS_COMMAND: {'name': ['proxy', 'Modbus_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'modbus_cmd_', 'fmt': FMT_INT, 'name': 'Modbus Command', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
# 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':FMT_FLOAT,'name': 'Grid Voltage'}}, # noqa: E501
|
||||
|
||||
|
||||
136
app/src/mqtt.py
Normal file → Executable file
136
app/src/mqtt.py
Normal file → Executable file
@@ -2,6 +2,8 @@ import asyncio
|
||||
import logging
|
||||
import aiomqtt
|
||||
import traceback
|
||||
import struct
|
||||
import inspect
|
||||
|
||||
from modbus import Modbus
|
||||
from messages import Message
|
||||
@@ -27,14 +29,27 @@ class Mqtt(metaclass=Singleton):
|
||||
loop = asyncio.get_event_loop()
|
||||
self.task = loop.create_task(self.__loop())
|
||||
self.ha_restarts = 0
|
||||
self.topic_defs = [
|
||||
{'prefix': 'auto_conf_prefix', 'topic': '/status',
|
||||
'fnc': self._ha_status, 'args': []},
|
||||
{'prefix': 'entity_prefix', 'topic': '/+/rated_load',
|
||||
'fnc': self._modbus_cmd,
|
||||
'args': [Modbus.WRITE_SINGLE_REG, 1, 0x2008]},
|
||||
{'prefix': 'entity_prefix', 'topic': '/+/out_coeff',
|
||||
'fnc': self._out_coeff, 'args': []},
|
||||
{'prefix': 'entity_prefix', 'topic': '/+/dcu_power',
|
||||
'fnc': self._dcu_cmd, 'args': []},
|
||||
{'prefix': 'entity_prefix', 'topic': '/+/modbus_read_regs',
|
||||
'fnc': self._modbus_cmd, 'args': [Modbus.READ_REGS, 2]},
|
||||
{'prefix': 'entity_prefix', 'topic': '/+/modbus_read_inputs',
|
||||
'fnc': self._modbus_cmd, 'args': [Modbus.READ_INPUTS, 2]},
|
||||
{'prefix': 'entity_prefix', 'topic': '/+/at_cmd',
|
||||
'fnc': self._at_cmd, 'args': []},
|
||||
]
|
||||
|
||||
ha = Config.get('ha')
|
||||
self.ha_status_topic = f"{ha['auto_conf_prefix']}/status"
|
||||
self.mb_rated_topic = f"{ha['entity_prefix']}/+/rated_load"
|
||||
self.mb_out_coeff_topic = f"{ha['entity_prefix']}/+/out_coeff"
|
||||
self.mb_reads_topic = f"{ha['entity_prefix']}/+/modbus_read_regs"
|
||||
self.mb_inputs_topic = f"{ha['entity_prefix']}/+/modbus_read_inputs"
|
||||
self.mb_at_cmd_topic = f"{ha['entity_prefix']}/+/at_cmd"
|
||||
for entry in self.topic_defs:
|
||||
entry['full_topic'] = f"{ha[entry['prefix']]}{entry['topic']}"
|
||||
|
||||
@property
|
||||
def ha_restarts(self):
|
||||
@@ -75,19 +90,7 @@ class Mqtt(metaclass=Singleton):
|
||||
try:
|
||||
async with self.__client:
|
||||
logger_mqtt.info('MQTT broker connection established')
|
||||
self.ctime = datetime.now()
|
||||
self.published = 0
|
||||
self.received = 0
|
||||
|
||||
if self.__cb_mqtt_is_up:
|
||||
await self.__cb_mqtt_is_up()
|
||||
|
||||
await self.__client.subscribe(self.ha_status_topic)
|
||||
await self.__client.subscribe(self.mb_rated_topic)
|
||||
await self.__client.subscribe(self.mb_out_coeff_topic)
|
||||
await self.__client.subscribe(self.mb_reads_topic)
|
||||
await self.__client.subscribe(self.mb_inputs_topic)
|
||||
await self.__client.subscribe(self.mb_at_cmd_topic)
|
||||
await self._init_new_conn()
|
||||
|
||||
async for message in self.__client.messages:
|
||||
await self.dispatch_msg(message)
|
||||
@@ -117,47 +120,51 @@ class Mqtt(metaclass=Singleton):
|
||||
f"Exception:\n"
|
||||
f"{traceback.format_exc()}")
|
||||
|
||||
async def _init_new_conn(self):
|
||||
self.ctime = datetime.now()
|
||||
self.published = 0
|
||||
self.received = 0
|
||||
if self.__cb_mqtt_is_up:
|
||||
await self.__cb_mqtt_is_up()
|
||||
for entry in self.topic_defs:
|
||||
await self.__client.subscribe(entry['full_topic'])
|
||||
|
||||
async def dispatch_msg(self, message):
|
||||
self.received += 1
|
||||
|
||||
if message.topic.matches(self.ha_status_topic):
|
||||
status = message.payload.decode("UTF-8")
|
||||
logger_mqtt.info('Home-Assistant Status:'
|
||||
f' {status}')
|
||||
if status == 'online':
|
||||
self.ha_restarts += 1
|
||||
for entry in self.topic_defs:
|
||||
if message.topic.matches(entry['full_topic']) \
|
||||
and 'fnc' in entry:
|
||||
fnc = entry['fnc']
|
||||
|
||||
if inspect.iscoroutinefunction(fnc):
|
||||
await entry['fnc'](message, *entry['args'])
|
||||
elif callable(fnc):
|
||||
entry['fnc'](message, *entry['args'])
|
||||
|
||||
async def _ha_status(self, message):
|
||||
status = message.payload.decode("UTF-8")
|
||||
logger_mqtt.info('Home-Assistant Status:'
|
||||
f' {status}')
|
||||
if status == 'online':
|
||||
self.ha_restarts += 1
|
||||
if self.__cb_mqtt_is_up:
|
||||
await self.__cb_mqtt_is_up()
|
||||
|
||||
if message.topic.matches(self.mb_rated_topic):
|
||||
await self.modbus_cmd(message,
|
||||
Modbus.WRITE_SINGLE_REG,
|
||||
1, 0x2008)
|
||||
|
||||
if message.topic.matches(self.mb_out_coeff_topic):
|
||||
payload = message.payload.decode("UTF-8")
|
||||
try:
|
||||
val = round(float(payload) * 1024/100)
|
||||
if val < 0 or val > 1024:
|
||||
logger_mqtt.error('out_coeff: value must be in'
|
||||
'the range 0..100,'
|
||||
f' got: {payload}')
|
||||
else:
|
||||
await self.modbus_cmd(message,
|
||||
Modbus.WRITE_SINGLE_REG,
|
||||
0, 0x202c, val)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if message.topic.matches(self.mb_reads_topic):
|
||||
await self.modbus_cmd(message,
|
||||
Modbus.READ_REGS, 2)
|
||||
|
||||
if message.topic.matches(self.mb_inputs_topic):
|
||||
await self.modbus_cmd(message,
|
||||
Modbus.READ_INPUTS, 2)
|
||||
|
||||
if message.topic.matches(self.mb_at_cmd_topic):
|
||||
await self.at_cmd(message)
|
||||
async def _out_coeff(self, message):
|
||||
payload = message.payload.decode("UTF-8")
|
||||
try:
|
||||
val = round(float(payload) * 1024/100)
|
||||
if val < 0 or val > 1024:
|
||||
logger_mqtt.error('out_coeff: value must be in'
|
||||
'the range 0..100,'
|
||||
f' got: {payload}')
|
||||
else:
|
||||
await self._modbus_cmd(message,
|
||||
Modbus.WRITE_SINGLE_REG,
|
||||
0, 0x202c, val)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def each_inverter(self, message, func_name: str):
|
||||
topic = str(message.topic)
|
||||
@@ -175,7 +182,7 @@ class Mqtt(metaclass=Singleton):
|
||||
else:
|
||||
logger_mqtt.warning(f'Node_id: {node_id} not found')
|
||||
|
||||
async def modbus_cmd(self, message, func, params=0, addr=0, val=0):
|
||||
async def _modbus_cmd(self, message, func, params=0, addr=0, val=0):
|
||||
payload = message.payload.decode("UTF-8")
|
||||
for fnc in self.each_inverter(message, "send_modbus_cmd"):
|
||||
res = payload.split(',')
|
||||
@@ -190,7 +197,22 @@ class Mqtt(metaclass=Singleton):
|
||||
val = int(res[1]) # lenght
|
||||
await fnc(func, addr, val, logging.INFO)
|
||||
|
||||
async def at_cmd(self, message):
|
||||
async def _at_cmd(self, message):
|
||||
payload = message.payload.decode("UTF-8")
|
||||
for fnc in self.each_inverter(message, "send_at_cmd"):
|
||||
await fnc(payload)
|
||||
|
||||
def _dcu_cmd(self, message):
|
||||
payload = message.payload.decode("UTF-8")
|
||||
try:
|
||||
val = round(float(payload) * 10)
|
||||
if val < 1000 or val > 8000:
|
||||
logger_mqtt.error('dcu_power: value must be in'
|
||||
'the range 100..800,'
|
||||
f' got: {payload}')
|
||||
else:
|
||||
pdu = struct.pack('>BBBBBBH', 1, 1, 6, 1, 0, 1, val)
|
||||
for fnc in self.each_inverter(message, "send_dcu_cmd"):
|
||||
fnc(pdu)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user