add dcu_power MQTT topic

This commit is contained in:
Stefan Allius
2025-05-18 15:55:46 +02:00
parent c1bdec0844
commit 1491e42913
4 changed files with 146 additions and 0 deletions

19
app/src/gen3plus/solarman_v5.py Normal file → Executable file
View File

@@ -247,6 +247,7 @@ class SolarmanBase(Message):
class SolarmanV5(SolarmanBase): class SolarmanV5(SolarmanBase):
AT_CMD = 1 AT_CMD = 1
MB_RTU_CMD = 2 MB_RTU_CMD = 2
DCU_CMD = 5
AT_CMD_RSP = 8 AT_CMD_RSP = 8
MB_CLIENT_DATA_UP = 30 MB_CLIENT_DATA_UP = 30
'''Data up time in client mode''' '''Data up time in client mode'''
@@ -532,6 +533,24 @@ class SolarmanV5(SolarmanBase):
except Exception: except Exception:
self.ifc.tx_clear() 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._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): def __forward_msg(self):
self.forward(self.ifc.rx_peek(), self.header_len+self.data_len+2) self.forward(self.ifc.rx_peek(), self.header_len+self.data_len+2)

21
app/src/mqtt.py Normal file → Executable file
View File

@@ -2,6 +2,7 @@ import asyncio
import logging import logging
import aiomqtt import aiomqtt
import traceback import traceback
import struct
from modbus import Modbus from modbus import Modbus
from messages import Message from messages import Message
@@ -32,6 +33,7 @@ class Mqtt(metaclass=Singleton):
self.ha_status_topic = f"{ha['auto_conf_prefix']}/status" self.ha_status_topic = f"{ha['auto_conf_prefix']}/status"
self.mb_rated_topic = f"{ha['entity_prefix']}/+/rated_load" self.mb_rated_topic = f"{ha['entity_prefix']}/+/rated_load"
self.mb_out_coeff_topic = f"{ha['entity_prefix']}/+/out_coeff" self.mb_out_coeff_topic = f"{ha['entity_prefix']}/+/out_coeff"
self.dcu_power_topic = f"{ha['entity_prefix']}/+/dcu_power"
self.mb_reads_topic = f"{ha['entity_prefix']}/+/modbus_read_regs" self.mb_reads_topic = f"{ha['entity_prefix']}/+/modbus_read_regs"
self.mb_inputs_topic = f"{ha['entity_prefix']}/+/modbus_read_inputs" self.mb_inputs_topic = f"{ha['entity_prefix']}/+/modbus_read_inputs"
self.mb_at_cmd_topic = f"{ha['entity_prefix']}/+/at_cmd" self.mb_at_cmd_topic = f"{ha['entity_prefix']}/+/at_cmd"
@@ -85,6 +87,7 @@ class Mqtt(metaclass=Singleton):
await self.__client.subscribe(self.ha_status_topic) await self.__client.subscribe(self.ha_status_topic)
await self.__client.subscribe(self.mb_rated_topic) await self.__client.subscribe(self.mb_rated_topic)
await self.__client.subscribe(self.mb_out_coeff_topic) await self.__client.subscribe(self.mb_out_coeff_topic)
await self.__client.subscribe(self.dcu_power_topic)
await self.__client.subscribe(self.mb_reads_topic) await self.__client.subscribe(self.mb_reads_topic)
await self.__client.subscribe(self.mb_inputs_topic) await self.__client.subscribe(self.mb_inputs_topic)
await self.__client.subscribe(self.mb_at_cmd_topic) await self.__client.subscribe(self.mb_at_cmd_topic)
@@ -148,6 +151,9 @@ class Mqtt(metaclass=Singleton):
except Exception: except Exception:
pass pass
if message.topic.matches(self.dcu_power_topic):
self.dcu_cmd(message)
if message.topic.matches(self.mb_reads_topic): if message.topic.matches(self.mb_reads_topic):
await self.modbus_cmd(message, await self.modbus_cmd(message,
Modbus.READ_REGS, 2) Modbus.READ_REGS, 2)
@@ -194,3 +200,18 @@ class Mqtt(metaclass=Singleton):
payload = message.payload.decode("UTF-8") payload = message.payload.decode("UTF-8")
for fnc in self.each_inverter(message, "send_at_cmd"): for fnc in self.each_inverter(message, "send_at_cmd"):
await fnc(payload) 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

20
app/tests/test_mqtt.py Normal file → Executable file
View File

@@ -69,6 +69,14 @@ def spy_modbus_cmd_client():
yield wrapped_conn yield wrapped_conn
conn.close() conn.close()
@pytest.fixture
def spy_dcu_cmd():
conn = SolarmanV5(None, ('test.local', 1234), server_side=True, client_mode= False, ifc=AsyncIfcImpl())
conn.node_id = 'inv_3/'
with patch.object(conn, 'send_dcu_cmd', wraps=conn.send_dcu_cmd) as wrapped_conn:
yield wrapped_conn
conn.close()
def test_native_client(test_hostname, test_port): def test_native_client(test_hostname, test_port):
"""Sanity check: Make sure the paho-mqtt client can connect to the test """Sanity check: Make sure the paho-mqtt client can connect to the test
MQTT server. Otherwise the test set NO_MOSQUITTO_TEST to True and disable MQTT server. Otherwise the test set NO_MOSQUITTO_TEST to True and disable
@@ -267,3 +275,15 @@ async def test_at_cmd_dispatch(config_mqtt_conn, spy_at_cmd):
finally: finally:
await m.close() await m.close()
@pytest.mark.asyncio
async def test_dcu_dispatch(config_mqtt_conn, spy_dcu_cmd):
_ = config_mqtt_conn
spy = spy_dcu_cmd
try:
m = Mqtt(None)
msg = aiomqtt.Message(topic= 'tsun/inv_3/dcu_power', payload= b'100.0', qos= 0, retain = False, mid= 0, properties= None)
await m.dispatch_msg(msg)
spy.assert_called_once_with(b'\x01\x01\x06\x01\x00\x01\x03\xe8')
finally:
await m.close()

86
app/tests/test_solarman.py Normal file → Executable file
View File

@@ -812,6 +812,15 @@ def dcu_data_rsp_msg(): # 0x1210
msg += b'\x15' msg += b'\x15'
return msg return msg
@pytest.fixture
def dcu_command_ind_msg(): # 0x4510
msg = b'\xa5\x17\x00\x10\x45\x94\x02' +get_dcu_sn() +b'\x05\x26\x30'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
msg += b'\x01\x01\x06\x01\x00\x01\x03\xe8'
msg += correct_checksum(msg)
msg += b'\x15'
return msg
@pytest.fixture @pytest.fixture
def config_tsun_allow_all(): def config_tsun_allow_all():
Config.act_config = { Config.act_config = {
@@ -2402,3 +2411,80 @@ async def test_proxy_at_blocked(my_loop, config_tsun_inv1, patch_open_connection
assert Proxy.mqtt.key == 'tsun/inv1/at_resp' assert Proxy.mqtt.key == 'tsun/inv1/at_resp'
assert Proxy.mqtt.data == "+ok" assert Proxy.mqtt.data == "+ok"
@pytest.mark.asyncio
async def test_dcu_cmd(my_loop, config_tsun_allow_all, dcu_dev_ind_msg, dcu_dev_rsp_msg, dcu_data_ind_msg, dcu_data_rsp_msg, dcu_command_ind_msg, at_command_rsp_msg):
_ = config_tsun_allow_all
m = MemoryStream(dcu_dev_ind_msg, (0,), True)
m.read() # read device ind
assert m.control == 0x4110
assert str(m.seq) == '01:92'
assert m.ifc.tx_fifo.get()==dcu_dev_rsp_msg
assert m.ifc.fwd_fifo.get()==dcu_dev_ind_msg
m.send_dcu_cmd(b'\x01\x01\x06\x01\x00\x01\x03\xe8')
assert m.ifc.tx_fifo.get()==b''
assert m.ifc.fwd_fifo.get()==b''
assert m.sent_pdu == b''
assert str(m.seq) == '01:92'
assert Proxy.mqtt.key == ''
assert Proxy.mqtt.data == ""
m.append_msg(dcu_data_ind_msg)
m.read() # read inverter ind
assert m.control == 0x4210
assert str(m.seq) == '02:93'
assert m.ifc.tx_fifo.get()==dcu_data_rsp_msg
assert m.ifc.fwd_fifo.get()==dcu_data_ind_msg
m.send_dcu_cmd(b'\x01\x01\x06\x01\x00\x01\x03\xe8')
assert m.ifc.fwd_fifo.get() == b''
assert m.ifc.tx_fifo.get()== b''
assert m.sent_pdu == dcu_command_ind_msg
m.sent_pdu = bytearray()
assert str(m.seq) == '02:94'
assert Proxy.mqtt.key == ''
assert Proxy.mqtt.data == ""
# m.append_msg(at_command_rsp_msg)
# m.read() # read at resp
# assert m.control == 0x1510
# assert str(m.seq) == '03:03'
# assert m.ifc.rx_get()==b''
# assert m.ifc.tx_fifo.get()==b''
# assert m.ifc.fwd_fifo.get()==b''
# assert Proxy.mqtt.key == 'tsun/at_resp'
# assert Proxy.mqtt.data == "+ok"
Proxy.mqtt.clear() # clear last test result
@pytest.mark.asyncio
async def test_dcu_cmd_not_supported(my_loop, config_tsun_allow_all, device_ind_msg, device_rsp_msg, inverter_ind_msg, inverter_rsp_msg):
_ = config_tsun_allow_all
m = MemoryStream(device_ind_msg, (0,), True)
m.read() # read device ind
assert m.control == 0x4110
assert str(m.seq) == '01:01'
assert m.ifc.tx_fifo.get()==device_rsp_msg
assert m.ifc.fwd_fifo.get()==device_ind_msg
m.send_dcu_cmd(b'\x01\x01\x06\x01\x00\x01\x03\xe8')
assert m.ifc.tx_fifo.get()==b''
assert m.ifc.fwd_fifo.get()==b''
assert m.sent_pdu == b''
assert str(m.seq) == '01:01'
assert Proxy.mqtt.key == ''
assert Proxy.mqtt.data == ""
m.append_msg(inverter_ind_msg)
m.read() # read inverter ind
assert m.control == 0x4210
assert str(m.seq) == '02:02'
assert m.ifc.tx_fifo.get()==inverter_rsp_msg
assert m.ifc.fwd_fifo.get()==inverter_ind_msg
m.send_dcu_cmd(b'\x01\x01\x06\x01\x00\x01\x03\xe8')
assert m.ifc.fwd_fifo.get() == b''
assert m.ifc.tx_fifo.get()== b''
assert m.sent_pdu == b''
Proxy.mqtt.clear() # clear last test result