Compare commits
37 Commits
s-allius/i
...
s-allius/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e629488963 | ||
|
|
ac828921c6 | ||
|
|
6744e03a90 | ||
|
|
dc1a28260e | ||
|
|
e59529adc0 | ||
|
|
8d93b2a636 | ||
|
|
01e9e70957 | ||
|
|
1721bbebe2 | ||
|
|
41168fbb4d | ||
|
|
25ba6ef8f3 | ||
|
|
2a40bd7b71 | ||
|
|
95182d2196 | ||
|
|
f1da544c88 | ||
|
|
7365980c2f | ||
|
|
11e3226460 | ||
|
|
f69f9c6d63 | ||
|
|
321c66838d | ||
|
|
0a8e708735 | ||
|
|
bd88647f0b | ||
|
|
bb2250bca1 | ||
|
|
e25aa5f922 | ||
|
|
e78395c269 | ||
|
|
55665c95f8 | ||
|
|
51aec15148 | ||
|
|
dc9e90d049 | ||
|
|
b98b133d79 | ||
|
|
2eab7fec3e | ||
|
|
fb4fe6b34d | ||
|
|
bfd7dbe032 | ||
|
|
25e4714fa5 | ||
|
|
46d1b77e56 | ||
|
|
dc360f31d6 | ||
|
|
2e9d16b611 | ||
|
|
1491e42913 | ||
|
|
07b90da09e | ||
|
|
b2d9da2c09 | ||
|
|
cf1a87ed6f |
4
.github/workflows/python-app.yml
vendored
4
.github/workflows/python-app.yml
vendored
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.13.2
|
||||
3.13.4
|
||||
|
||||
12
CHANGELOG.md
12
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
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.14.0
|
||||
0.15.0
|
||||
@@ -1,8 +1,8 @@
|
||||
flake8==7.2.0
|
||||
pytest==8.3.5
|
||||
pytest-asyncio==0.26.0
|
||||
pytest-cov==6.1.1
|
||||
pytest==8.4.0
|
||||
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
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<button href="#" class="w3-bar-item w3-button w3-padding-16 w3-hide-large w3-dark-grey w3-hover-black" onclick="w3_close()" title="close menu"><i class="fa fa-remove fa-fw"></i> Close Menu</button>
|
||||
<a href="{{ url_for('.index')}}" class="w3-bar-item w3-button w3-padding {% block menu1_class %}{% endblock %}"><i class="fa fa-network-wired fa-fw"></i> {{_('Connections')}}</a>
|
||||
<a href="{{ url_for('.mqtt')}}" class="w3-bar-item w3-button w3-padding {% block menu2_class %}{% endblock %}"><i class="fa fa-database fa-fw"></i> MQTT</a>
|
||||
<a href="{{ url_for('.notes')}}" class="w3-bar-item w3-button w3-padding {% block menu3_class %}{% endblock %}"><i class="fa fa-exclamation-triangle fa-fw"></i> {{_('Important Messages')}}</a>
|
||||
<a href="{{ url_for('.notes')}}" class="w3-bar-item w3-button w3-padding {% block menu3_class %}{% endblock %}"><i class="fa fa-info fa-fw"></i> {{_('Important Messages')}}</a>
|
||||
<a href="{{ url_for('.logging')}}" class="w3-bar-item w3-button w3-padding {% block menu4_class %}{% endblock %}"><i class="fa fa-file-export fa-fw"></i> {{_('Log Files')}}</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
{% block title %}{{_("TSUN Proxy - Important Messages")}}{% endblock title %}
|
||||
{% block menu3_class %}w3-blue{% endblock %}
|
||||
{% block headline %}<i class="fa fa-exclamation-triangle fa-fw"></i> {{_('Important Messages')}}{% endblock headline %}
|
||||
{% block headline %}<i class="fa fa-info fa-fw"></i> {{_('Important Messages')}}{% endblock headline %}
|
||||
{% block content %}
|
||||
<div id="notes-list"></div>
|
||||
{% endblock content%}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: tsun-gen3-proxy 0.14.0\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-05-13 22:34+0200\n"
|
||||
"POT-Creation-Date: 2025-05-18 15:30+0200\n"
|
||||
"PO-Revision-Date: 2025-04-18 16:24+0200\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: de\n"
|
||||
@@ -120,6 +120,7 @@ msgid "TSUN Proxy - Log Files"
|
||||
msgstr "TSUN Proxy - Log Dateien"
|
||||
|
||||
#: src/web/templates/page_logging.html.j2:10
|
||||
#, python-format
|
||||
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?"
|
||||
|
||||
|
||||
@@ -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 $@
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user