diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index ae65cd7..c67a313 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -5,7 +5,7 @@ name: Python application on: push: - branches: [ "main", "dev-*", "*/issue*" ] + branches: [ "main", "dev-*", "*/issue*", "releases/*" ] paths-ignore: - '**.md' # Do no build on *.md changes - '**.yml' # Do no build on *.yml changes @@ -18,7 +18,7 @@ on: - '**.dockerfile' # Do no build on *.dockerfile changes - '**.sh' # Do no build on *.sh changes pull_request: - branches: [ "main", "dev-*" ] + branches: [ "main", "dev-*", "releases/*" ] permissions: contents: read diff --git a/.python-version b/.python-version index 3e388a4..0d9339e 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.13.2 +3.13.4 diff --git a/CHANGELOG.md b/CHANGELOG.md index df55052..5626fea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- Update dependency pytest-asyncio to v1 + +## [0.14.1] - 2025-05-31 + +- handle missing MQTT addon [#438](https://github.com/s-allius/tsun-gen3-proxy/issues/438) + +## [0.14.0] - 2025-05-29 + +- add-on: bump python to version 3.12.10-r1 +- set no of pv modules for MS800 GEN3PLUS inverters +- 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 - Update ghcr.io/hassio-addons/base Docker tag to v17.2.5 - fix a lot of pytest-asyncio problems in the unit tests diff --git a/app/.version b/app/.version index 0548fb4..7092c7c 100644 --- a/app/.version +++ b/app/.version @@ -1 +1 @@ -0.14.0 \ No newline at end of file +0.15.0 \ No newline at end of file diff --git a/app/requirements-test.txt b/app/requirements-test.txt index 2bfde03..32ce135 100644 --- a/app/requirements-test.txt +++ b/app/requirements-test.txt @@ -1,8 +1,8 @@ flake8==7.2.0 pytest==8.4.0 - pytest-asyncio==0.26.0 - pytest-cov==6.1.1 + pytest-asyncio==1.0.0 + pytest-cov==6.2.1 python-dotenv==1.1.0 mock==5.2.0 - coverage==7.8.0 + coverage==7.9.0 jinja2-cli==0.8.2 \ No newline at end of file diff --git a/app/src/cnf/config.py b/app/src/cnf/config.py index 5207c7d..207fbe8 100644 --- a/app/src/cnf/config.py +++ b/app/src/cnf/config.py @@ -162,7 +162,8 @@ class Config(): ) @classmethod - def init(cls, def_reader: ConfigIfc, log_path: str = '') -> None | str: + def init(cls, def_reader: ConfigIfc, log_path: str = '', + cnf_path: str = 'config') -> None | str: '''Initialise the Proxy-Config Copy the internal default config file into the config directory @@ -173,12 +174,13 @@ and initialise the Config with the default configuration ''' try: # make the default config transparaent by copying it # in the config.example file - logging.debug('Copy Default Config to config.example.toml') + logging.info( + f'Copy Default Config to {cnf_path}config.example.toml') - shutil.copy2("default_config.toml", - "config/config.example.toml") - except Exception: - pass + shutil.copy2("cnf/default_config.toml", + cnf_path + "config.example.toml") + except Exception as e: + logging.error(e) # read example config file as default configuration try: diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index 470c695..086dcd3 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -216,7 +216,7 @@ class InfosG3P(Infos): self.set_db_def_value(Register.MANUFACTURER, 'TSUN') self.set_db_def_value(Register.EQUIPMENT_MODEL, 'TSOL-MSxx00') self.set_db_def_value(Register.CHIP_TYPE, 'IGEN TECH') - self.set_db_def_value(Register.NO_INPUTS, 4) + self.set_db_def_value(Register.NO_INPUTS, 2) def __hide_topic(self, row: dict) -> bool: if 'dep' in row: diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index 2cf7d16..355890b 100755 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -562,12 +562,17 @@ class SolarmanV5(SolarmanBase): rated = db.get_db_value(Register.RATED_POWER, 0) model = None if max_pow == 2000: + db.set_db_def_value(Register.NO_INPUTS, 4) if rated == 800 or rated == 600: model = f'TSOL-MS{max_pow}({rated})' else: model = f'TSOL-MS{max_pow}' elif max_pow == 1800 or max_pow == 1600: + db.set_db_def_value(Register.NO_INPUTS, 4) model = f'TSOL-MS{max_pow}' + elif max_pow <= 800: + model = f'TSOL-MS{max_pow}' + if model: logger.info(f'Model: {model}') self.db.set_db_def_value(Register.EQUIPMENT_MODEL, model) diff --git a/app/src/scheduler.py b/app/src/scheduler.py index 3c1d25a..5445310 100644 --- a/app/src/scheduler.py +++ b/app/src/scheduler.py @@ -12,7 +12,7 @@ class Schedule: count = 0 @classmethod - def start(cls) -> None: + def start(cls) -> None: # pragma: no cover '''Start the scheduler and schedule the tasks (cron jobs)''' logging.debug("Scheduler init") cls.mqtt = Mqtt(None) @@ -20,7 +20,7 @@ class Schedule: crontab('0 0 * * *', func=cls.atmidnight, start=True) @classmethod - async def atmidnight(cls) -> None: + async def atmidnight(cls) -> None: # pragma: no cover '''Clear daily counters at midnight''' logging.info("Clear daily counters at midnight") diff --git a/app/src/server.py b/app/src/server.py index 26ee093..60a2fe8 100644 --- a/app/src/server.py +++ b/app/src/server.py @@ -127,7 +127,8 @@ class Server(): def build_config(self): # read config file Config.init(ConfigReadToml(self.src_dir + "cnf/default_config.toml"), - log_path=self.log_path) + log_path=self.log_path, + cnf_path=self.config_path) ConfigReadEnv() ConfigReadJson(self.config_path + "config.json") ConfigReadToml(self.config_path + "config.toml") diff --git a/app/src/web/templates/base.html.j2 b/app/src/web/templates/base.html.j2 index 0ef1f7e..51268af 100644 --- a/app/src/web/templates/base.html.j2 +++ b/app/src/web/templates/base.html.j2 @@ -57,7 +57,7 @@   {{_('Connections')}}   MQTT -   {{_('Important Messages')}} +   {{_('Important Messages')}}   {{_('Log Files')}} diff --git a/app/src/web/templates/page_notes.html.j2 b/app/src/web/templates/page_notes.html.j2 index 495e5e2..e501705 100644 --- a/app/src/web/templates/page_notes.html.j2 +++ b/app/src/web/templates/page_notes.html.j2 @@ -2,7 +2,7 @@ {% block title %}{{_("TSUN Proxy - Important Messages")}}{% endblock title %} {% block menu3_class %}w3-blue{% endblock %} -{% block headline %}  {{_('Important Messages')}}{% endblock headline %} +{% block headline %}  {{_('Important Messages')}}{% endblock headline %} {% block content %}
{% endblock content%} diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index 0596044..95e24a8 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -109,7 +109,7 @@ def test_default_db(): i = InfosG3P(client_mode=False) assert json.dumps(i.db) == json.dumps({ - "inverter": {"Manufacturer": "TSUN", "Equipment_Model": "TSOL-MSxx00", "No_Inputs": 4}, + "inverter": {"Manufacturer": "TSUN", "Equipment_Model": "TSOL-MSxx00", "No_Inputs": 2}, "collector": {"Chip_Type": "IGEN TECH"}, }) @@ -271,7 +271,7 @@ def test_build_ha_conf1(): elif id == 'inv_count_456': assert False - assert tests==7 + assert tests==5 def test_build_ha_conf2(): i = InfosG3P(client_mode=False) @@ -346,7 +346,7 @@ def test_build_ha_conf3(): elif id == 'inv_count_456': assert False - assert tests==7 + assert tests==5 def test_build_ha_conf4(): i = InfosG3P(client_mode=True) diff --git a/app/tests/test_server.py b/app/tests/test_server.py index e13ee5f..7b0e934 100644 --- a/app/tests/test_server.py +++ b/app/tests/test_server.py @@ -4,6 +4,10 @@ import logging import os from mock import patch from server import app, Server, ProxyState, HypercornLogHndl +from inverter_base import InverterBase +from gen3.talent import Talent + +from test_inverter_base import FakeReader, FakeWriter pytest_plugins = ('pytest_asyncio',) @@ -108,20 +112,20 @@ class TestServerClass: assert logging.getLogger('hypercorn.access').level == logging.INFO assert logging.getLogger('hypercorn.error').level == logging.INFO - os.environ["LOG_LVL"] = "WARN" - s.parse_args(['--log_backups', '3']) - s.init_logging_system() - assert s.log_backups == 3 - assert s.log_level == logging.WARNING - assert logging.handlers.log_backups == 3 - assert logging.getLogger().level == s.log_level - assert logging.getLogger('msg').level == s.log_level - assert logging.getLogger('conn').level == s.log_level - assert logging.getLogger('data').level == s.log_level - assert logging.getLogger('tracer').level == s.log_level - assert logging.getLogger('asyncio').level == s.log_level - assert logging.getLogger('hypercorn.access').level == logging.INFO - assert logging.getLogger('hypercorn.error').level == logging.INFO + with patch.dict(os.environ, {'LOG_LVL': 'WARN'}): + s.parse_args(['--log_backups', '3']) + s.init_logging_system() + assert s.log_backups == 3 + assert s.log_level == logging.WARNING + assert logging.handlers.log_backups == 3 + assert logging.getLogger().level == s.log_level + assert logging.getLogger('msg').level == s.log_level + assert logging.getLogger('conn').level == s.log_level + assert logging.getLogger('data').level == s.log_level + assert logging.getLogger('tracer').level == s.log_level + assert logging.getLogger('asyncio').level == s.log_level + assert logging.getLogger('hypercorn.access').level == logging.INFO + assert logging.getLogger('hypercorn.error').level == logging.INFO def test_build_config_error(self, caplog): s = self.FakeServer() @@ -202,17 +206,81 @@ class TestApp: @pytest.mark.asyncio async def test_healthy(self): """Test the healthy route.""" + reader = FakeReader() + writer = FakeWriter() - ProxyState.set_up(False) - client = app.test_client() - response = await client.get('/-/healthy') - assert response.status_code == 200 - result = await response.get_data() - assert result == b"I'm fine" + with InverterBase(reader, writer, 'tsun', Talent): + ProxyState.set_up(False) + client = app.test_client() + response = await client.get('/-/healthy') + assert response.status_code == 200 + result = await response.get_data() + assert result == b"I'm fine" - ProxyState.set_up(True) - response = await client.get('/-/healthy') - assert response.status_code == 200 - result = await response.get_data() - assert result == b"I'm fine" + ProxyState.set_up(True) + response = await client.get('/-/healthy') + assert response.status_code == 200 + result = await response.get_data() + assert result == b"I'm fine" + @pytest.mark.asyncio + async def test_unhealthy(self, monkeypatch, caplog): + """Test the healthy route.""" + def result_false(self): + return False + + LOGGER = logging.getLogger("mqtt") + LOGGER.propagate = True + LOGGER.setLevel(logging.INFO) + + monkeypatch.setattr(InverterBase, "healthy", result_false) + InverterBase._registry.clear() + reader = FakeReader() + writer = FakeWriter() + + with caplog.at_level(logging.INFO) and InverterBase(reader, writer, 'tsun', Talent): + ProxyState.set_up(False) + client = app.test_client() + response = await client.get('/-/healthy') + assert response.status_code == 200 + result = await response.get_data() + assert result == b"I'm fine" + assert "" == caplog.text + + ProxyState.set_up(True) + response = await client.get('/-/healthy') + assert response.status_code == 503 + result = await response.get_data() + assert result == b"I have a problem" + assert "" == caplog.text + + @pytest.mark.asyncio + async def test_healthy_exception(self, monkeypatch, caplog): + """Test the healthy route.""" + def result_except(self): + raise ValueError + + LOGGER = logging.getLogger("mqtt") + LOGGER.propagate = True + LOGGER.setLevel(logging.INFO) + + monkeypatch.setattr(InverterBase, "healthy", result_except) + InverterBase._registry.clear() + reader = FakeReader() + writer = FakeWriter() + + with caplog.at_level(logging.INFO) and InverterBase(reader, writer, 'tsun', Talent): + ProxyState.set_up(False) + client = app.test_client() + response = await client.get('/-/healthy') + assert response.status_code == 200 + result = await response.get_data() + assert result == b"I'm fine" + assert "" == caplog.text + + ProxyState.set_up(True) + response = await client.get('/-/healthy') + assert response.status_code == 200 + result = await response.get_data() + assert result == b"I'm fine" + assert "Exception:" in caplog.text diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py index 77866cf..141b058 100755 --- a/app/tests/test_solarman.py +++ b/app/tests/test_solarman.py @@ -462,6 +462,39 @@ def inverter_ind_msg800(): # 0x4210 rated Power 800W msg += b'\x15' return msg +@pytest.fixture +def inverter_ind_msg900(): # 0x4210 rated Power 900W + msg = b'\xa5\x99\x01\x10\x42\xe6\x9e' +get_sn() +b'\x01\xb0\x02\xbc\xc8' + msg += b'\x24\x32\x6c\x1f\x00\x00\xa0\x47\xe4\x33\x01\x00\x03\x08\x00\x00' + msg += b'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x40\x10\x08\xc8\x00\x49\x13\x8d\x00\x36\x00\x00\x03\x84\x06\x7a' + msg += b'\x01\x61\x00\xa8\x02\x54\x01\x5a\x00\x8a\x01\xe4\x01\x5a\x00\xbd' + msg += b'\x02\x8f\x00\x11\x00\x01\x00\x00\x00\x0b\x00\x00\x27\x98\x00\x04' + msg += b'\x00\x00\x0c\x04\x00\x03\x00\x00\x0a\xe7\x00\x05\x00\x00\x0c\x75' + msg += b'\x00\x00\x00\x00\x06\x16\x02\x00\x00\x00\x55\xaa\x00\x01\x00\x00' + msg += b'\x00\x00\x00\x00\xff\xff\x03\x84\x00\x03\x04\x00\x04\x00\x04\x00' + msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00' + msg += b'\x09\xcd\x07\xb6\x13\x9c\x13\x24\x00\x01\x07\xae\x04\x0f\x00\x41' + msg += b'\x00\x0f\x0a\x64\x0a\x64\x00\x06\x00\x06\x09\xf6\x12\x8c\x12\x8c' + msg += b'\x00\x10\x00\x10\x14\x52\x14\x52\x00\x10\x00\x10\x01\x51\x00\x05' + msg += b'\x04\x00\x00\x01\x13\x9c\x0f\xa0\x00\x4e\x00\x66\x03\xe8\x04\x00' + msg += b'\x09\xce\x07\xa8\x13\x9c\x13\x26\x00\x00\x00\x00\x00\x00\x00\x00' + msg += b'\x00\x00\x00\x00\x04\x00\x04\x00\x00\x00\x00\x00\xff\xff\x00\x00' + msg += b'\x00\x00\x00\x00' + msg += correct_checksum(msg) + msg += b'\x15' + return msg + @pytest.fixture def inverter_ind_msg_81(): # 0x4210 fcode 0x81 msg = b'\xa5\x99\x01\x10\x42\x02\x03' +get_sn() +b'\x81\xb0\x02\xbc\xc8' @@ -676,6 +709,19 @@ def msg_modbus_rsp(): # 0x1510 msg += b'\x15' return msg +@pytest.fixture +def msg_modbus_rsp_mb_4(): # 0x1510, MODBUS Type:4 + msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x01' + msg += total() + msg += hb() + msg += b'\x0a\xe2\xfa\x33\x01\x04\x28\x40\x10\x08\xd8' + msg += b'\x00\x00\x13\x87\x00\x31\x00\x68\x02\x58\x00\x00\x01\x53\x00\x02' + msg += b'\x00\x00\x01\x52\x00\x02\x00\x00\x01\x53\x00\x03\x00\x00\x00\x04' + msg += b'\x00\x01\x00\x00\x9e\xa4' + msg += correct_checksum(msg) + msg += b'\x15' + return msg + @pytest.fixture def msg_modbus_interim_rsp(): # 0x0510 msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x01' @@ -1435,6 +1481,7 @@ async def test_build_modell_600(my_loop, config_tsun_allow_all, inverter_ind_msg m.read() # read complete msg, and dispatch msg assert 2000 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0) assert 600 == m.db.get_db_value(Register.RATED_POWER, 0) + assert 4 == m.db.get_db_value(Register.NO_INPUTS, 0) assert 'TSOL-MS2000(600)' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0) assert '02b0' == m.db.get_db_value(Register.SENSOR_LIST, None) assert 0 == m.sensor_list # must not been set by an inverter data ind @@ -1454,6 +1501,7 @@ async def test_build_modell_1600(my_loop, config_tsun_allow_all, inverter_ind_ms m.read() # read complete msg, and dispatch msg assert 1600 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0) assert 1600 == m.db.get_db_value(Register.RATED_POWER, 0) + assert 4 == m.db.get_db_value(Register.NO_INPUTS, 0) assert 'TSOL-MS1600' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0) m.close() @@ -1467,6 +1515,7 @@ async def test_build_modell_1800(my_loop, config_tsun_allow_all, inverter_ind_ms m.read() # read complete msg, and dispatch msg assert 1800 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0) assert 1800 == m.db.get_db_value(Register.RATED_POWER, 0) + assert 4 == m.db.get_db_value(Register.NO_INPUTS, 0) assert 'TSOL-MS1800' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0) m.close() @@ -1480,6 +1529,7 @@ async def test_build_modell_2000(my_loop, config_tsun_allow_all, inverter_ind_ms m.read() # read complete msg, and dispatch msg assert 2000 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0) assert 2000 == m.db.get_db_value(Register.RATED_POWER, 0) + assert 4 == m.db.get_db_value(Register.NO_INPUTS, 0) assert 'TSOL-MS2000' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0) m.close() @@ -1493,6 +1543,21 @@ async def test_build_modell_800(my_loop, config_tsun_allow_all, inverter_ind_msg m.read() # read complete msg, and dispatch msg assert 800 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0) assert 800 == m.db.get_db_value(Register.RATED_POWER, 0) + assert 2 == m.db.get_db_value(Register.NO_INPUTS, 0) + assert 'TSOL-MS800' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0) + m.close() + +@pytest.mark.asyncio +async def test_build_modell_900(my_loop, config_tsun_allow_all, inverter_ind_msg900): + _ = config_tsun_allow_all + m = MemoryStream(inverter_ind_msg900, (0,)) + assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0) + assert None == m.db.get_db_value(Register.RATED_POWER, None) + assert None == m.db.get_db_value(Register.INVERTER_TEMP, None) + m.read() # read complete msg, and dispatch msg + assert 900 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0) + assert 900 == m.db.get_db_value(Register.RATED_POWER, 0) + assert 2 == m.db.get_db_value(Register.NO_INPUTS, 0) assert 'TSOL-MSxx00' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0) m.close() @@ -2189,6 +2254,61 @@ async def test_modbus_scaning(config_tsun_scan, heartbeat_ind_msg, heartbeat_rsp assert next(m.mb_timer.exp_count) == 3 m.close() +@pytest.mark.asyncio +async def test_modbus_scaning_inv_rsp(config_tsun_scan, heartbeat_ind_msg, heartbeat_rsp_msg, msg_modbus_rsp_mb_4): + _ = config_tsun_scan + assert asyncio.get_running_loop() + + m = MemoryStream(heartbeat_ind_msg, (0x15,0x56,0)) + m.append_msg(msg_modbus_rsp_mb_4) + assert m.mb_scan == False + assert asyncio.get_running_loop() == m.mb_timer.loop + m.db.stat['proxy']['Unknown_Ctrl'] = 0 + assert m.mb_timer.tim == None + m.read() # read complete msg, and dispatch msg + assert m.mb_scan == True + assert m.mb_start_reg == 0xff80 + assert m.mb_step == 0x40 + assert m.mb_bytes == 0x14 + assert asyncio.get_running_loop() == m.mb_timer.loop + + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 1 + assert m.snr == 2070233889 + assert m.control == 0x4710 + + assert m.msg_recvd[0]['control']==0x4710 + assert m.msg_recvd[0]['seq']=='84:11' + assert m.msg_recvd[0]['data_len']==0x1 + + assert m.ifc.tx_fifo.get()==heartbeat_rsp_msg + assert m.ifc.fwd_fifo.get()==heartbeat_ind_msg + assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 + + m.ifc.tx_clear() # clear send buffer for next test + assert isclose(m.mb_timeout, 0.5) + assert next(m.mb_timer.exp_count) == 0 + + await asyncio.sleep(0.5) + assert m.sent_pdu==b'\xa5\x17\x00\x10E\x12\x84!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\x00\x01\x03\xff\xc0\x00\x14\x75\xed\x33\x15' + assert m.ifc.tx_fifo.get()==b'' + + m.read() # read complete msg, and dispatch msg + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 2 + assert m.msg_recvd[1]['control']==0x1510 + assert m.msg_recvd[1]['seq']=='03:03' + assert m.msg_recvd[1]['data_len']==0x3b + assert m.mb.last_addr == 1 + assert m.mb.last_fcode == 3 + assert m.mb.last_reg == 0xffc0 # mb_start_reg + mb_step + assert m.mb.last_len == 20 + assert m.mb.err == 3 + + assert next(m.mb_timer.exp_count) == 2 + m.close() + @pytest.mark.asyncio async def test_start_client_mode(my_loop, config_tsun_inv1, str_test_ip): _ = config_tsun_inv1 diff --git a/ha_addons/Makefile b/ha_addons/Makefile index b1e0ccb..61aecad 100644 --- a/ha_addons/Makefile +++ b/ha_addons/Makefile @@ -192,7 +192,7 @@ $(repro_all_subdirs) : mkdir -p $@ $(repro_all_templates) : $(INST_BASE)/ha_addon_%/config.yaml: $(TEMPL)/config.jinja $(TEMPL)/%_data.json $(SRC)/.version FORCE - $(JINJA) --strict -D AppVersion=$(VERSION)-$* -D BuildID=$(BUILD_ID) $< $(filter %.json,$^) -o $@ + $(JINJA) --strict -D AppVersion=$(VERSION)-$*$(RC) -D BuildID=$(BUILD_ID) $< $(filter %.json,$^) -o $@ $(repro_all_apparmor) : $(INST_BASE)/ha_addon_%/apparmor.txt: $(TEMPL)/apparmor.jinja $(TEMPL)/%_data.json $(JINJA) --strict $< $(filter %.json,$^) -o $@ diff --git a/ha_addons/ha_addon/Dockerfile b/ha_addons/ha_addon/Dockerfile index 3557e13..a23882f 100755 --- a/ha_addons/ha_addon/Dockerfile +++ b/ha_addons/ha_addon/Dockerfile @@ -18,7 +18,7 @@ ARG BUILD_FROM="ghcr.io/hassio-addons/base:17.2.5" FROM $BUILD_FROM AS base # Installiere Python, pip und virtuelle Umgebungstools -RUN apk add --no-cache python3=3.12.10-r0 py3-pip=24.3.1-r0 && \ +RUN apk add --no-cache python3=3.12.10-r1 py3-pip=24.3.1-r0 && \ python -m venv /opt/venv && \ . /opt/venv/bin/activate diff --git a/ha_addons/ha_addon/rootfs/run.sh b/ha_addons/ha_addon/rootfs/run.sh index 6c231e4..c2dc223 100755 --- a/ha_addons/ha_addon/rootfs/run.sh +++ b/ha_addons/ha_addon/rootfs/run.sh @@ -1,18 +1,28 @@ #!/usr/bin/with-contenv bashio -echo "Add-on environment started" - -echo "check for Home Assistant MQTT" -MQTT_HOST=$(bashio::services mqtt "host") -MQTT_PORT=$(bashio::services mqtt "port") -MQTT_USER=$(bashio::services mqtt "username") -MQTT_PASSWORD=$(bashio::services mqtt "password") +bashio::log.blue "-----------------------------------------------------------" +bashio::log.blue "run.sh: info: setup Add-on environment" +bashio::cache.flush_all +MQTT_HOST="" +if bashio::supervisor.ping; then + bashio::log "run.sh: info: check for Home Assistant MQTT service" + if bashio::services.available mqtt; then + MQTT_HOST=$(bashio::services mqtt "host") + MQTT_PORT=$(bashio::services mqtt "port") + MQTT_USER=$(bashio::services mqtt "username") + MQTT_PASSWORD=$(bashio::services mqtt "password") + else + bashio::log.yellow "run.sh: info: Home Assistant MQTT service not available!" + fi +else + bashio::log.red "run.sh: error: Home Assistant Supervisor API not available!" +fi # if a MQTT was/not found, drop a note if [ -z "$MQTT_HOST" ]; then - echo "MQTT not found" + bashio::log.yellow "run.sh: info: MQTT config not found" else - echo "MQTT found" + bashio::log.green "run.sh: info: MQTT config found" export MQTT_HOST export MQTT_PORT export MQTT_USER @@ -29,5 +39,6 @@ cd /home/proxy || exit export VERSION=$(cat /proxy-version.txt) -echo "Start Proxyserver..." +bashio::log.blue "run.sh: info: Start Proxyserver..." +bashio::log.blue "-----------------------------------------------------------" python3 server.py --rel_urls --json_config=/data/options.json --log_path=/homeassistant/tsun-proxy/logs/ --config_path=/homeassistant/tsun-proxy/ --log_backups=2 diff --git a/ha_addons/templates/rc_data.json b/ha_addons/templates/rc_data.json index 05d33ce..b9e5fd4 100644 --- a/ha_addons/templates/rc_data.json +++ b/ha_addons/templates/rc_data.json @@ -2,7 +2,6 @@ { "name": "TSUN-Proxy (Release Candidate)", "description": "MQTT Proxy for TSUN Photovoltaic Inverters", - "version": "rc", "image": "ghcr.io/s-allius/tsun-gen3-addon", "slug": "tsun-proxy-rc", "advanced": true,