Compare commits

..

8 Commits

Author SHA1 Message Date
Stefan Allius
33edfc7832 new version 0.14.1 2025-05-31 22:47:13 +02:00
Stefan Allius
1471509977 update version 0.14.1 2025-05-31 21:46:29 +02:00
Stefan Allius
3e53ca928b revise the log outputs 2025-05-31 21:45:48 +02:00
Stefan Allius
67c1f5e71a avoid printing of the MQTT config inkl. password 2025-05-31 19:25:07 +02:00
Stefan Allius
75a95b7916 run also on releases/* branch 2025-05-31 19:00:56 +02:00
Stefan Allius
68397246cd handle missing MQTT addon 2025-05-31 18:58:14 +02:00
Stefan Allius
ffaef139a2 handle missing MQTT addon
- we have to check if the supervisor API and a
MQTT broker add-on is installed. If not we assume
the user has an external MQTT broker
2025-05-31 18:54:22 +02:00
Stefan Allius
4fda544b19 Update change log (#436)
* S allius/issue427 (#434)

* mock the aiomqtt library and increse coverage

* test inv response for a mb scan request

* improve test coverage

* S allius/issue427 (#435)

* mock the aiomqtt library and increse coverage

* test inv response for a mb scan request

* improve test coverage

* improve test case

* version 0.14.0
2025-05-29 22:43:06 +02:00
7 changed files with 100 additions and 53 deletions

View File

@@ -1 +1 @@
3.13.4 3.13.2

View File

@@ -5,10 +5,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [unreleased]
- Update dependency pytest-asyncio to v1
## [0.14.1] - 2025-05-31 ## [0.14.1] - 2025-05-31
- handle missing MQTT addon [#438](https://github.com/s-allius/tsun-gen3-proxy/issues/438) - handle missing MQTT addon [#438](https://github.com/s-allius/tsun-gen3-proxy/issues/438)
@@ -19,7 +15,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- set no of pv modules for MS800 GEN3PLUS inverters - set no of pv modules for MS800 GEN3PLUS inverters
- fix the paths to copy the config.example.toml file during proxy start - fix the paths to copy the config.example.toml file during proxy start
- add MQTT topic `dcu_power` for setting output power on DCUs - add MQTT topic `dcu_power` for setting output power on DCUs
- add MQTT topic `dcu_power` for setting output power on DCUs
- Update ghcr.io/hassio-addons/base Docker tag to v17.2.5 - Update ghcr.io/hassio-addons/base Docker tag to v17.2.5
- fix a lot of pytest-asyncio problems in the unit tests - fix a lot of pytest-asyncio problems in the unit tests
- Cleanup startup code for Quart and the Proxy - Cleanup startup code for Quart and the Proxy

View File

@@ -1 +1 @@
0.15.0 0.14.1

View File

@@ -1,8 +1,8 @@
flake8==7.2.0 flake8==7.2.0
pytest==8.4.0 pytest==8.3.5
pytest-asyncio==1.0.0 pytest-asyncio==0.26.0
pytest-cov==6.2.1 pytest-cov==6.1.1
python-dotenv==1.1.0 python-dotenv==1.1.0
mock==5.2.0 mock==5.2.0
coverage==7.9.0 coverage==7.8.2
jinja2-cli==0.8.2 jinja2-cli==0.8.2

View File

@@ -3,7 +3,8 @@ import pytest
import asyncio import asyncio
import aiomqtt import aiomqtt
import logging import logging
from aiomqtt import MqttError from aiomqtt import MqttError, MessagesIterator
from aiomqtt import Message as AiomqttMessage
from mock import patch, Mock from mock import patch, Mock
from async_stream import AsyncIfcImpl from async_stream import AsyncIfcImpl
@@ -34,6 +35,26 @@ def test_hostname():
# else: # else:
return 'test.mosquitto.org' return 'test.mosquitto.org'
@pytest.fixture(scope="function")
def aiomqtt_mock(monkeypatch):
recv_que = asyncio.Queue()
async def my_aenter(self):
return self
async def my_subscribe(self, *arg):
return
async def my_anext(self):
return await recv_que.get()
async def my_receive(self, topic: str, payload: bytes):
msg = AiomqttMessage(topic, payload,qos=0, retain=False, mid=0, properties=None)
await recv_que.put(msg)
await asyncio.sleep(0) # dispath the msg
monkeypatch.setattr(aiomqtt.Client, "__aenter__", my_aenter)
monkeypatch.setattr(aiomqtt.Client, "subscribe", my_subscribe)
monkeypatch.setattr(MessagesIterator, "__anext__", my_anext)
monkeypatch.setattr(Mqtt, "receive", my_receive, False)
@pytest.fixture @pytest.fixture
def config_mqtt_conn(test_hostname, test_port): def config_mqtt_conn(test_hostname, test_port):
Config.act_config = {'mqtt':{'host': test_hostname, 'port': test_port, 'user': '', 'passwd': ''}, Config.act_config = {'mqtt':{'host': test_hostname, 'port': test_port, 'user': '', 'passwd': ''},
@@ -161,13 +182,17 @@ async def test_ha_reconnect(config_mqtt_conn):
await m.close() await m.close()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_mqtt_no_config(config_no_conn): async def test_mqtt_no_config(config_no_conn, monkeypatch):
_ = config_no_conn _ = config_no_conn
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
on_connect = asyncio.Event() on_connect = asyncio.Event()
async def cb(): async def cb():
on_connect.set() on_connect.set()
async def my_publish(*args):
return
monkeypatch.setattr(aiomqtt.Client, "publish", my_publish)
try: try:
m = Mqtt(cb) m = Mqtt(cb)
@@ -176,9 +201,9 @@ async def test_mqtt_no_config(config_no_conn):
assert not on_connect.is_set() assert not on_connect.is_set()
try: try:
await m.publish('homeassistant/status', 'online') await m.publish('homeassistant/status', 'online')
assert False assert m.published == 1
except Exception: except Exception:
pass assert False
except TimeoutError: except TimeoutError:
assert False assert False
finally: finally:
@@ -250,92 +275,119 @@ async def test_mqtt_except_def_config(config_def_conn, monkeypatch, caplog):
assert 'MQTT is unconfigured; Check your config.toml!' in caplog.text assert 'MQTT is unconfigured; Check your config.toml!' in caplog.text
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_msg_dispatch(config_mqtt_conn, spy_modbus_cmd): async def test_mqtt_dispatch(config_mqtt_conn, aiomqtt_mock, spy_modbus_cmd):
_ = config_mqtt_conn _ = config_mqtt_conn
_ = aiomqtt_mock
spy = spy_modbus_cmd spy = spy_modbus_cmd
try: try:
m = Mqtt(None) m = Mqtt(None)
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'online', qos= 0, retain = False, mid= 0, properties= None) assert m.ha_restarts == 0
await m.dispatch_msg(msg) await m.receive('homeassistant/status', b'online') # send the message
assert m.ha_restarts == 1 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.receive(topic= 'tsun/inv_1/rated_load', payload= b'2')
await m.dispatch_msg(msg)
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x2008, 2, logging.INFO) spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x2008, 2, logging.INFO)
spy.reset_mock() spy.reset_mock()
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'100', qos= 0, retain = False, mid= 0, properties= None) await m.receive(topic= 'tsun/inv_1/out_coeff', payload= b'100')
await m.dispatch_msg(msg)
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x202c, 1024, logging.INFO) spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x202c, 1024, logging.INFO)
spy.reset_mock() spy.reset_mock()
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'50', qos= 0, retain = False, mid= 0, properties= None) await m.receive(topic= 'tsun/inv_1/out_coeff', payload= b'50')
await m.dispatch_msg(msg)
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x202c, 512, logging.INFO) spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x202c, 512, logging.INFO)
spy.reset_mock() spy.reset_mock()
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_regs', payload= b'0x3000, 10', qos= 0, retain = False, mid= 0, properties= None) await m.receive(topic= 'tsun/inv_1/modbus_read_regs', payload= b'0x3000, 10')
await m.dispatch_msg(msg)
spy.assert_awaited_once_with(Modbus.READ_REGS, 0x3000, 10, logging.INFO) spy.assert_awaited_once_with(Modbus.READ_REGS, 0x3000, 10, logging.INFO)
spy.reset_mock() 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.receive(topic= 'tsun/inv_1/modbus_read_inputs', payload= b'0x3000, 10')
await m.dispatch_msg(msg)
spy.assert_awaited_once_with(Modbus.READ_INPUTS, 0x3000, 10, logging.INFO) spy.assert_awaited_once_with(Modbus.READ_INPUTS, 0x3000, 10, logging.INFO)
# test dispatching with empty mapping table # test dispatching with empty mapping table
m.topic_defs.clear() m.topic_defs.clear()
spy.reset_mock() 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.receive(topic= 'tsun/inv_1/modbus_read_inputs', payload= b'0x3000, 10')
await m.dispatch_msg(msg)
spy.assert_not_called() spy.assert_not_called()
# test dispatching with incomplete mapping table - invalid fnc defined # test dispatching with incomplete mapping table - invalid fnc defined
m.topic_defs.append( m.topic_defs.append(
{'prefix': 'entity_prefix', 'topic': '/+/modbus_read_inputs', {'prefix': 'entity_prefix', 'topic': '/+/modbus_read_inputs',
'full_topic': 'tsun/+/modbus_read_inputs', 'fnc': 'invalid'} 'full_topic': 'tsun/+/modbus_read_inputs', 'fnc': 'addr'}
) )
spy.reset_mock() 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.receive(topic= 'tsun/inv_1/modbus_read_inputs', payload= b'0x3000, 10')
await m.dispatch_msg(msg)
spy.assert_not_called() spy.assert_not_called()
except MqttError:
assert False
except Exception:
assert False
finally: finally:
await m.close() await m.close()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_msg_dispatch_err(config_mqtt_conn, spy_modbus_cmd): async def test_mqtt_dispatch_cb(config_mqtt_conn, aiomqtt_mock):
_ = config_mqtt_conn _ = config_mqtt_conn
_ = aiomqtt_mock
on_connect = asyncio.Event()
async def cb():
on_connect.set()
try:
m = Mqtt(cb)
assert m.ha_restarts == 0
await m.receive('homeassistant/status', b'online') # send the message
assert on_connect.is_set()
assert m.ha_restarts == 1
except MqttError:
assert False
except Exception:
assert False
finally:
await m.close()
@pytest.mark.asyncio
async def test_mqtt_dispatch_err(config_mqtt_conn, aiomqtt_mock, spy_modbus_cmd, caplog):
_ = config_mqtt_conn
_ = aiomqtt_mock
spy = spy_modbus_cmd spy = spy_modbus_cmd
LOGGER = logging.getLogger("mqtt")
LOGGER.propagate = True
LOGGER.setLevel(logging.INFO)
try: try:
m = Mqtt(None) m = Mqtt(None)
# test out of range param # test out of range param
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'-1', qos= 0, retain = False, mid= 0, properties= None) await m.receive(topic= 'tsun/inv_1/out_coeff', payload= b'-1')
await m.dispatch_msg(msg)
spy.assert_not_called() spy.assert_not_called()
# test unknown node_id # test unknown node_id
spy.reset_mock() await m.receive(topic= 'tsun/inv_2/out_coeff', payload= b'2')
msg = aiomqtt.Message(topic= 'tsun/inv_2/out_coeff', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
await m.dispatch_msg(msg)
spy.assert_not_called() spy.assert_not_called()
# test invalid fload param # test invalid fload param
spy.reset_mock() await m.receive(topic= 'tsun/inv_1/out_coeff', payload= b'2, 3')
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'2, 3', qos= 0, retain = False, mid= 0, properties= None) spy.assert_not_called()
await m.dispatch_msg(msg)
await m.receive(topic= 'tsun/inv_1/modbus_read_regs', payload= b'0x3000, 10, 7')
spy.assert_not_called() spy.assert_not_called()
spy.reset_mock() await m.receive(topic= 'tsun/inv_1/dcu_power', payload= b'100W')
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() spy.assert_not_called()
with caplog.at_level(logging.INFO):
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
for _ in m.each_inverter(msg, "addr"):
pass # do nothing here
assert 'Cmd not supported by: inv_1/' in caplog.text
except MqttError:
assert False
except Exception:
assert False
finally: finally:
await m.close() await m.close()

View File

@@ -2662,6 +2662,7 @@ async def test_proxy_dcu_cmd(my_loop, config_tsun_dcu1, patch_open_connection, d
assert l.db.stat['proxy']['AT_Command'] == 0 assert l.db.stat['proxy']['AT_Command'] == 0
assert l.db.stat['proxy']['AT_Command_Blocked'] == 0 assert l.db.stat['proxy']['AT_Command_Blocked'] == 0
assert l.db.stat['proxy']['Modbus_Command'] == 0 assert l.db.stat['proxy']['Modbus_Command'] == 0
assert 2 == l.db.get_db_value(Register.NO_INPUTS, 0)
l.append_msg(dcu_command_rsp_msg) l.append_msg(dcu_command_rsp_msg)
l.read() # read at resp l.read() # read at resp

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: tsun-gen3-proxy 0.14.0\n" "Project-Id-Version: tsun-gen3-proxy 0.14.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-05-18 15:30+0200\n" "POT-Creation-Date: 2025-05-13 22:34+0200\n"
"PO-Revision-Date: 2025-04-18 16:24+0200\n" "PO-Revision-Date: 2025-04-18 16:24+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de\n" "Language: de\n"
@@ -120,7 +120,6 @@ msgid "TSUN Proxy - Log Files"
msgstr "TSUN Proxy - Log Dateien" msgstr "TSUN Proxy - Log Dateien"
#: src/web/templates/page_logging.html.j2:10 #: src/web/templates/page_logging.html.j2:10
#, python-format
msgid "Do you really want to delete the log file: <br>%(file)s ?" msgid "Do you really want to delete the log file: <br>%(file)s ?"
msgstr "Soll die Datei: <br>%(file)s<br>wirklich gelöscht werden?" msgstr "Soll die Datei: <br>%(file)s<br>wirklich gelöscht werden?"