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:
@@ -17,13 +17,13 @@ def test_statistic_counter():
|
||||
assert val == None or val == 0
|
||||
|
||||
i.static_init() # initialize counter
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 0, "Cloud_Conn_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "Modbus_Command": 0}})
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 0, "Cloud_Conn_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "DCU_Command": 0, "Modbus_Command": 0}})
|
||||
|
||||
val = i.dev_value(Register.INVERTER_CNT) # valid and initiliazed addr
|
||||
assert val == 0
|
||||
|
||||
i.inc_counter('Inverter_Cnt')
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 1, "Cloud_Conn_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "Modbus_Command": 0}})
|
||||
assert json.dumps(i.stat) == json.dumps({"proxy": {"Inverter_Cnt": 1, "Cloud_Conn_Cnt": 0, "Unknown_SNR": 0, "Unknown_Msg": 0, "Invalid_Data_Type": 0, "Internal_Error": 0,"Unknown_Ctrl": 0, "OTA_Start_Msg": 0, "SW_Exception": 0, "Invalid_Msg_Format": 0, "AT_Command": 0, "AT_Command_Blocked": 0, "DCU_Command": 0, "Modbus_Command": 0}})
|
||||
val = i.dev_value(Register.INVERTER_CNT)
|
||||
assert val == 1
|
||||
|
||||
|
||||
141
app/tests/test_mqtt.py
Normal file → Executable file
141
app/tests/test_mqtt.py
Normal file → Executable file
@@ -3,8 +3,9 @@ import pytest
|
||||
import asyncio
|
||||
import aiomqtt
|
||||
import logging
|
||||
|
||||
from aiomqtt import MqttError
|
||||
from mock import patch, Mock
|
||||
|
||||
from async_stream import AsyncIfcImpl
|
||||
from singleton import Singleton
|
||||
from mqtt import Mqtt
|
||||
@@ -17,7 +18,7 @@ NO_MOSQUITTO_TEST = False
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def module_init():
|
||||
Singleton._instances.clear()
|
||||
yield
|
||||
@@ -44,6 +45,14 @@ def config_no_conn(test_port):
|
||||
Config.act_config = {'mqtt':{'host': "", 'port': test_port, 'user': '', 'passwd': ''},
|
||||
'ha':{'auto_conf_prefix': 'homeassistant','discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun'}
|
||||
}
|
||||
Config.def_config = {}
|
||||
|
||||
@pytest.fixture
|
||||
def config_def_conn(test_port):
|
||||
Config.act_config = {'mqtt':{'host': "unknown_url", 'port': test_port, 'user': '', 'passwd': ''},
|
||||
'ha':{'auto_conf_prefix': 'homeassistant','discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun'}
|
||||
}
|
||||
Config.def_config = Config.act_config
|
||||
|
||||
@pytest.fixture
|
||||
def spy_at_cmd():
|
||||
@@ -69,6 +78,14 @@ def spy_modbus_cmd_client():
|
||||
yield wrapped_conn
|
||||
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):
|
||||
"""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
|
||||
@@ -167,12 +184,81 @@ async def test_mqtt_no_config(config_no_conn):
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_except_no_config(config_no_conn, monkeypatch, caplog):
|
||||
_ = config_no_conn
|
||||
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
async def my_aenter(self):
|
||||
raise MqttError('TestException') from None
|
||||
|
||||
monkeypatch.setattr(aiomqtt.Client, "__aenter__", my_aenter)
|
||||
|
||||
LOGGER = logging.getLogger("mqtt")
|
||||
LOGGER.propagate = True
|
||||
LOGGER.setLevel(logging.INFO)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
m = Mqtt(None)
|
||||
assert m.task
|
||||
await asyncio.sleep(0)
|
||||
try:
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
assert False
|
||||
except MqttError:
|
||||
pass
|
||||
except Exception:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
assert 'Connection lost; Reconnecting in 5 seconds' in caplog.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_except_def_config(config_def_conn, monkeypatch, caplog):
|
||||
_ = config_def_conn
|
||||
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
async def my_aenter(self):
|
||||
raise MqttError('TestException') from None
|
||||
|
||||
monkeypatch.setattr(aiomqtt.Client, "__aenter__", my_aenter)
|
||||
|
||||
LOGGER = logging.getLogger("mqtt")
|
||||
LOGGER.propagate = True
|
||||
LOGGER.setLevel(logging.INFO)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
m = Mqtt(cb)
|
||||
assert m.task
|
||||
await asyncio.sleep(0)
|
||||
assert not on_connect.is_set()
|
||||
try:
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
assert False
|
||||
except MqttError:
|
||||
pass
|
||||
except Exception:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
assert 'MQTT is unconfigured; Check your config.toml!' in caplog.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_msg_dispatch(config_mqtt_conn, spy_modbus_cmd):
|
||||
_ = config_mqtt_conn
|
||||
spy = spy_modbus_cmd
|
||||
try:
|
||||
m = Mqtt(None)
|
||||
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'online', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
assert m.ha_restarts == 1
|
||||
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/rated_load', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x2008, 2, logging.INFO)
|
||||
@@ -197,6 +283,23 @@ async def test_msg_dispatch(config_mqtt_conn, spy_modbus_cmd):
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_awaited_once_with(Modbus.READ_INPUTS, 0x3000, 10, logging.INFO)
|
||||
|
||||
# test dispatching with empty mapping table
|
||||
m.topic_defs.clear()
|
||||
spy.reset_mock()
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_inputs', payload= b'0x3000, 10', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
|
||||
# test dispatching with incomplete mapping table - invalid fnc defined
|
||||
m.topic_defs.append(
|
||||
{'prefix': 'entity_prefix', 'topic': '/+/modbus_read_inputs',
|
||||
'full_topic': 'tsun/+/modbus_read_inputs', 'fnc': 'invalid'}
|
||||
)
|
||||
spy.reset_mock()
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_inputs', payload= b'0x3000, 10', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@@ -227,6 +330,12 @@ async def test_msg_dispatch_err(config_mqtt_conn, spy_modbus_cmd):
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_regs', payload= b'0x3000, 10, 7', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
|
||||
spy.reset_mock()
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/dcu_power', payload= b'100W', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@@ -267,3 +376,31 @@ async def test_at_cmd_dispatch(config_mqtt_conn, spy_at_cmd):
|
||||
|
||||
finally:
|
||||
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()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dcu_inv_value(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'99.9', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
|
||||
msg = aiomqtt.Message(topic= 'tsun/inv_3/dcu_power', payload= b'800.1', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
spy.assert_not_called()
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
152
app/tests/test_solarman.py
Normal file → Executable file
152
app/tests/test_solarman.py
Normal file → Executable file
@@ -812,6 +812,26 @@ def dcu_data_rsp_msg(): # 0x1210
|
||||
msg += b'\x15'
|
||||
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
|
||||
def dcu_command_rsp_msg(): # 0x1510
|
||||
msg = b'\xa5\x11\x00\x10\x15\x94\x03' +get_dcu_sn() +b'\x05\x01'
|
||||
msg += total()
|
||||
msg += hb()
|
||||
msg += b'\x00\x00\x00\x00'
|
||||
msg += b'\x01\x01\x01'
|
||||
msg += correct_checksum(msg)
|
||||
msg += b'\x15'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def config_tsun_allow_all():
|
||||
Config.act_config = {
|
||||
@@ -854,7 +874,17 @@ def config_tsun_scan_dcu():
|
||||
|
||||
@pytest.fixture
|
||||
def config_tsun_dcu1():
|
||||
Config.act_config = {'solarman':{'enabled': True},'batteries':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1/', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 0}}}
|
||||
Config.act_config = {
|
||||
'ha':{
|
||||
'auto_conf_prefix': 'homeassistant',
|
||||
'discovery_prefix': 'homeassistant',
|
||||
'entity_prefix': 'tsun',
|
||||
'proxy_node_id': 'test_1',
|
||||
'proxy_unique_id': ''
|
||||
},
|
||||
'solarman':{'enabled': True, 'host': 'test_cloud.local', 'port': 1234},'batteries':{'4100000000000001':{'monitor_sn': 2070233888, 'node_id':'inv1/', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 0}}}
|
||||
Proxy.class_init()
|
||||
Proxy.mqtt = Mqtt()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_message(device_ind_msg):
|
||||
@@ -2402,3 +2432,123 @@ 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.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, dcu_command_rsp_msg):
|
||||
'''test dcu_power command fpr a DCU device with sensor 0x3026'''
|
||||
_ = 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(dcu_command_rsp_msg)
|
||||
m.read() # read at resp
|
||||
assert m.control == 0x1510
|
||||
assert str(m.seq) == '03:94'
|
||||
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/dcu_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):
|
||||
'''test that an inverter don't accept the dcu_power command'''
|
||||
_ = 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
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_dcu_cmd(my_loop, config_tsun_dcu1, patch_open_connection, dcu_command_ind_msg, dcu_command_rsp_msg):
|
||||
_ = config_tsun_inv1
|
||||
_ = patch_open_connection
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
with InverterTest(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
r = inverter.remote.stream
|
||||
l = inverter.local.stream
|
||||
|
||||
l.db.stat['proxy']['DCU_Command'] = 0
|
||||
l.db.stat['proxy']['AT_Command'] = 0
|
||||
l.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||
l.db.stat['proxy']['AT_Command_Blocked'] = 0
|
||||
l.db.stat['proxy']['Modbus_Command'] = 0
|
||||
inverter.forward_dcu_cmd_resp = False
|
||||
r.append_msg(dcu_command_ind_msg)
|
||||
r.read() # read complete msg, and dispatch msg
|
||||
assert inverter.forward_dcu_cmd_resp
|
||||
inverter.forward(r,l)
|
||||
|
||||
assert l.ifc.tx_fifo.get()==dcu_command_ind_msg
|
||||
|
||||
assert l.db.stat['proxy']['Invalid_Msg_Format'] == 0
|
||||
assert l.db.stat['proxy']['DCU_Command'] == 1
|
||||
assert l.db.stat['proxy']['AT_Command'] == 0
|
||||
assert l.db.stat['proxy']['AT_Command_Blocked'] == 0
|
||||
assert l.db.stat['proxy']['Modbus_Command'] == 0
|
||||
|
||||
l.append_msg(dcu_command_rsp_msg)
|
||||
l.read() # read at resp
|
||||
assert l.ifc.fwd_fifo.peek()==dcu_command_rsp_msg
|
||||
inverter.forward(l,r)
|
||||
assert r.ifc.tx_fifo.get()==dcu_command_rsp_msg
|
||||
|
||||
assert Proxy.mqtt.key == ''
|
||||
assert Proxy.mqtt.data == ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user