Compare commits

...

14 Commits

Author SHA1 Message Date
Stefan Allius
57d0f25d4c build the README.md files for the HA Add-ons 2025-04-04 20:00:16 +02:00
Stefan Allius
bd2a526781 Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into s-allius/issue340 2025-04-04 18:47:54 +02:00
Stefan Allius
970b611d47 fix systemtest (#344) 2025-04-04 18:38:17 +02:00
Stefan Allius
38fe5f7e49 fix fix_systemtest 2025-04-04 18:32:37 +02:00
renovate[bot]
1ec97a3e9c Update ghcr.io/hassio-addons/base Docker tag to v17.2.3 (#342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>
2025-04-04 14:51:13 +02:00
renovate[bot]
2707582a45 Update dependency flake8 to v7.2.0 (#330)
* Update dependency flake8 to v7.2.0

* Flake8: ignore F821 errors, due of False Positives

# cleanup some unit tests

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>
2025-04-04 14:29:00 +02:00
renovate[bot]
bcec8dd843 Update dependency aiohttp to v3.11.16 (#341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 21:00:43 +02:00
renovate[bot]
1b5af7fa97 Update dependency pytest-cov to v6.1.0 (#339)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>
2025-04-01 23:17:42 +02:00
renovate[bot]
2731c68675 Update dependency aiohttp to v3.11.15 (#338)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>
2025-04-01 23:12:17 +02:00
renovate[bot]
a8f8eca06c Update dependency aiomqtt to v2.3.1 (#337)
* Update dependency aiomqtt to v2.3.1

* update aiomqtt badge

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Stefan Allius <stefan.allius@t-online.de>
2025-04-01 23:08:42 +02:00
renovate[bot]
f9eb4ad8d7 Update dependency coverage to v7.8.0 (#336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-01 22:54:40 +02:00
Stefan Allius
0e65e90c25 add DDZY422-D2 as not supported (#333)
* add DDZY422-D2 as not supported

* describe not supported devices clearer
2025-03-30 16:40:02 +02:00
renovate[bot]
18b2a2bfb2 Update dependency python-dotenv to v1.1.0 (#332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-30 01:13:17 +01:00
renovate[bot]
d1da8a85d3 Update dependency pytest-asyncio to v0.26.0 (#331)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-30 01:05:22 +01:00
19 changed files with 145 additions and 91 deletions

View File

@@ -6,4 +6,9 @@ PRIVAT_CONTAINER_REGISTRY=docker.io/<user>/
# registry for official container (preview, rc, rel) # registry for official container (preview, rc, rel)
PUBLIC_CONTAINER_REGISTRY=ghcr.io/<user>/ PUBLIC_CONTAINER_REGISTRY=ghcr.io/<user>/
PUBLIC_CR_KEY= PUBLIC_CR_KEY=
# define serial number of GEN3PLUS devices for systemtests
# the serialnumber are coded as 4-byte hex-strings
SOLARMAN_INV_SNR='00000000'
SOLARMAN_DCU_SNR='00000000'

View File

@@ -49,7 +49,7 @@ jobs:
- name: Lint with flake8 - name: Lint with flake8
run: | run: |
# stop the build if there are Python syntax errors or undefined names # stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics flake8 . --count --select=E9,F63,F7,F82 --ignore=F821 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 --exit-zero --ignore=C901,E121,E123,E126,E133,E226,E241,E242,E704,W503,W504,W505 --format=pylint --output-file=output_flake.txt --exclude=*.pyc app/src/ flake8 --exit-zero --ignore=C901,E121,E123,E126,E133,E226,E241,E242,E704,W503,W504,W505 --format=pylint --output-file=output_flake.txt --exclude=*.pyc app/src/
- name: Test with pytest - name: Test with pytest

View File

@@ -8,7 +8,7 @@
<p align="center"> <p align="center">
<a href="https://opensource.org/licenses/BSD-3-Clause"><img alt="License: BSD-3-Clause" src="https://img.shields.io/badge/License-BSD_3--Clause-green.svg"></a> <a href="https://opensource.org/licenses/BSD-3-Clause"><img alt="License: BSD-3-Clause" src="https://img.shields.io/badge/License-BSD_3--Clause-green.svg"></a>
<a href="https://www.python.org/downloads/release/python-3130/"><img alt="Supported Python versions" src="https://img.shields.io/badge/python-3.13-blue.svg"></a> <a href="https://www.python.org/downloads/release/python-3130/"><img alt="Supported Python versions" src="https://img.shields.io/badge/python-3.13-blue.svg"></a>
<a href="https://sbtinstruments.github.io/aiomqtt/introduction.html"><img alt="Supported aiomqtt versions" src="https://img.shields.io/badge/aiomqtt-2.3.0-lightblue.svg"></a> <a href="https://aiomqtt.bo3hm.com/introduction.html"><img alt="Supported aiomqtt versions" src="https://img.shields.io/badge/aiomqtt-2.3.1-lightblue.svg"></a>
<a href="https://libraries.io/pypi/aiocron"><img alt="Supported aiocron versions" src="https://img.shields.io/badge/aiocron-1.8-lightblue.svg"></a> <a href="https://libraries.io/pypi/aiocron"><img alt="Supported aiocron versions" src="https://img.shields.io/badge/aiocron-1.8-lightblue.svg"></a>
<a href="https://toml.io/en/v1.0.0"><img alt="Supported toml versions" src="https://img.shields.io/badge/toml-1.0.0-lightblue.svg"></a> <a href="https://toml.io/en/v1.0.0"><img alt="Supported toml versions" src="https://img.shields.io/badge/toml-1.0.0-lightblue.svg"></a>
<br> <br>
@@ -430,15 +430,15 @@ A combination with a red question mark should work, but I have not checked it in
<tr><td>GEN3 micro inverters (quad MPPT):<br>MS3000</td><td align="center">✔️</td><td align="center">✔️</td><td align="center">✔️</td><td align="center"></td><td align="center"></td></tr> <tr><td>GEN3 micro inverters (quad MPPT):<br>MS3000</td><td align="center">✔️</td><td align="center">✔️</td><td align="center">✔️</td><td align="center"></td><td align="center"></td></tr>
<tr><td>GEN3 PLUS micro inverters:<br>MS1600, MS1800, MS2000<br>MS2000-D, MS800</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center">✔️</td><td align="center">✔️</td></tr> <tr><td>GEN3 PLUS micro inverters:<br>MS1600, MS1800, MS2000<br>MS2000-D, MS800</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center">✔️</td><td align="center">✔️</td></tr>
<tr><td>GEN3 PLUS storage systems:<br>DC1000</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center">✔️</td><td align="center">✔️</td></tr> <tr><td>GEN3 PLUS storage systems:<br>DC1000</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center">✔️</td><td align="center">✔️</td></tr>
<tr><td>GEN3 PLUS smart meter:<br>TSOL-MG3-MS</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center">❓</td><td align="center">❓</td></tr> <tr><td>GEN3 PLUS smart meter:<br>TSOL-MG3-MS, DDZY422-D2</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center">❓</td><td align="center">❓</td></tr>
</<tr><td>TITAN micro inverters:<br>TSOL-MP3000, MP2250</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td></tr> </<tr><td>TITAN micro inverters:<br>TSOL-MP3000, MP2250</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td><td align="center">❓</td></tr>
</table> </table>
```txt ```txt
Legend Legend
: Firmware not available for this devices : Firmware not available for this devices
✔️: proxy support testet ✔️: Proxy support testet
❓: proxy support possible but not testet ❓: Proxy support unknown. There is an open port, but all known protocols do not work.
🚧: Proxy support in preparation 🚧: Proxy support in preparation
``` ```

View File

@@ -1,8 +1,8 @@
flake8==7.1.2 flake8==7.2.0
pytest==8.3.5 pytest==8.3.5
pytest-asyncio==0.25.3 pytest-asyncio==0.26.0
pytest-cov==6.0.0 pytest-cov==6.1.0
python-dotenv==1.0.1 python-dotenv==1.1.0
mock==5.2.0 mock==5.2.0
coverage==7.7.1 coverage==7.8.0
jinja2-cli==0.8.2 jinja2-cli==0.8.2

View File

@@ -1,4 +1,4 @@
aiomqtt==2.3.0 aiomqtt==2.3.1
schema==0.7.7 schema==0.7.7
aiocron==2.1 aiocron==2.1
aiohttp==3.11.14 aiohttp==3.11.16

View File

@@ -84,7 +84,10 @@ async def test_close_cb():
return 0.1 return 0.1
def closed(): def closed():
nonlocal cnt nonlocal cnt
nonlocal ifc # The callback will be called after the AsyncStreamServer
# constructer has finished and so ifc must be defined in the
# upper scope
assert "ifc" in locals()
ifc.close() # clears the closed callback ifc.close() # clears the closed callback
cnt += 1 cnt += 1
@@ -113,7 +116,6 @@ async def test_close_cb():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_read(): async def test_read():
global test
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
reader = FakeReader() reader = FakeReader()
reader.test = FakeReader.RD_TEST_13_BYTES reader.test = FakeReader.RD_TEST_13_BYTES
@@ -124,11 +126,13 @@ async def test_read():
return 1 return 1
def closed(): def closed():
nonlocal cnt nonlocal cnt
nonlocal ifc # The callback will be called after the AsyncStreamServer
# constructer has finished and so ifc must be defined in the
# upper scope
assert "ifc" in locals()
ifc.close() # clears the closed callback ifc.close() # clears the closed callback
cnt += 1 cnt += 1
def app_read(): def app_read():
nonlocal ifc
ifc.proc_start -= 3 ifc.proc_start -= 3
return 0.01 # async wait of 0.01 return 0.01 # async wait of 0.01
cnt = 0 cnt = 0
@@ -151,7 +155,6 @@ async def test_read():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_write(): async def test_write():
global test
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
reader = FakeReader() reader = FakeReader()
reader.test = FakeReader.RD_TEST_13_BYTES reader.test = FakeReader.RD_TEST_13_BYTES
@@ -162,11 +165,13 @@ async def test_write():
return 1 return 1
def closed(): def closed():
nonlocal cnt nonlocal cnt
nonlocal ifc # The callback will be called after the AsyncStreamServer
# constructer has finished and so ifc must be defined in the
# upper scope
assert "ifc" in locals()
ifc.close() # clears the closed callback ifc.close() # clears the closed callback
cnt += 1 cnt += 1
def app_read(): def app_read():
nonlocal ifc
ifc.proc_start -= 3 ifc.proc_start -= 3
return 0.01 # async wait of 0.01 return 0.01 # async wait of 0.01
@@ -203,7 +208,6 @@ async def test_publ_mqtt_cb():
return 0.1 return 0.1
async def publ_mqtt(): async def publ_mqtt():
nonlocal cnt nonlocal cnt
nonlocal ifc
cnt += 1 cnt += 1
cnt = 0 cnt = 0
@@ -233,7 +237,10 @@ async def test_create_remote_cb():
return 0.1 return 0.1
async def create_remote(): async def create_remote():
nonlocal cnt nonlocal cnt
nonlocal ifc # The callback will be called after the AsyncStreamServer
# constructer has finished and so ifc must be defined in the
# upper scope
assert "ifc" in locals()
ifc.close() # clears the closed callback ifc.close() # clears the closed callback
cnt += 1 cnt += 1
@@ -255,7 +262,6 @@ async def test_create_remote_cb():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_sw_exception(): async def test_sw_exception():
global test
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
reader = FakeReader() reader = FakeReader()
reader.test = FakeReader.RD_TEST_SW_EXCEPT reader.test = FakeReader.RD_TEST_SW_EXCEPT
@@ -266,7 +272,10 @@ async def test_sw_exception():
return 1 return 1
def closed(): def closed():
nonlocal cnt nonlocal cnt
nonlocal ifc # The callback will be called after the AsyncStreamServer
# constructer has finished and so ifc must be defined in the
# upper scope
assert "ifc" in locals()
ifc.close() # clears the closed callback ifc.close() # clears the closed callback
cnt += 1 cnt += 1
cnt = 0 cnt = 0
@@ -285,7 +294,6 @@ async def test_sw_exception():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_os_error(): async def test_os_error():
global test
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
reader = FakeReader() reader = FakeReader()
reader.test = FakeReader.RD_TEST_OS_ERROR reader.test = FakeReader.RD_TEST_OS_ERROR
@@ -293,12 +301,11 @@ async def test_os_error():
reader.on_recv.set() reader.on_recv.set()
writer = FakeWriter() writer = FakeWriter()
cnt = 0 cnt = 0
def timeout(): def timeout():
return 1 return 1
def closed(): def closed():
nonlocal cnt nonlocal cnt
nonlocal ifc
ifc.close() # clears the closed callback
cnt += 1 cnt += 1
cnt = 0 cnt = 0
ifc = AsyncStreamClient(reader, writer, None, closed) ifc = AsyncStreamClient(reader, writer, None, closed)
@@ -361,10 +368,13 @@ async def test_forward():
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
remote = StreamPtr(None) remote = StreamPtr(None)
cnt = 0 cnt = 0
async def _create_remote(): async def _create_remote():
nonlocal cnt, remote, ifc nonlocal cnt
create_remote(remote, TestType.FWD_NO_EXCPT) create_remote(remote, TestType.FWD_NO_EXCPT)
# The callback will be called after the AsyncStreamServer
# constructer has finished and so ifc must be defined in the
# upper scope
assert "ifc" in locals()
ifc.fwd_add(b'test-forward_msg2 ') ifc.fwd_add(b'test-forward_msg2 ')
cnt += 1 cnt += 1
@@ -382,7 +392,7 @@ async def test_forward_with_conn():
cnt = 0 cnt = 0
async def _create_remote(): async def _create_remote():
nonlocal cnt, remote, ifc nonlocal cnt
cnt += 1 cnt += 1
cnt = 0 cnt = 0
@@ -417,7 +427,7 @@ async def test_forward_sw_except():
cnt = 0 cnt = 0
async def _create_remote(): async def _create_remote():
nonlocal cnt, remote nonlocal cnt
create_remote(remote, TestType.FWD_SW_EXCPT) create_remote(remote, TestType.FWD_SW_EXCPT)
cnt += 1 cnt += 1
@@ -435,7 +445,7 @@ async def test_forward_os_error():
cnt = 0 cnt = 0
async def _create_remote(): async def _create_remote():
nonlocal cnt, remote nonlocal cnt
create_remote(remote, TestType.FWD_OS_ERROR) create_remote(remote, TestType.FWD_OS_ERROR)
cnt += 1 cnt += 1
@@ -453,7 +463,7 @@ async def test_forward_os_error2():
cnt = 0 cnt = 0
async def _create_remote(): async def _create_remote():
nonlocal cnt, remote nonlocal cnt
create_remote(remote, TestType.FWD_OS_ERROR, True) create_remote(remote, TestType.FWD_OS_ERROR, True)
cnt += 1 cnt += 1
@@ -471,7 +481,7 @@ async def test_forward_os_error3():
cnt = 0 cnt = 0
async def _create_remote(): async def _create_remote():
nonlocal cnt, remote nonlocal cnt
create_remote(remote, TestType.FWD_OS_ERROR_NO_STREAM) create_remote(remote, TestType.FWD_OS_ERROR_NO_STREAM)
cnt += 1 cnt += 1
@@ -489,7 +499,7 @@ async def test_forward_runtime_error():
cnt = 0 cnt = 0
async def _create_remote(): async def _create_remote():
nonlocal cnt, remote nonlocal cnt
create_remote(remote, TestType.FWD_RUNTIME_ERROR) create_remote(remote, TestType.FWD_RUNTIME_ERROR)
cnt += 1 cnt += 1
@@ -507,7 +517,7 @@ async def test_forward_runtime_error2():
cnt = 0 cnt = 0
async def _create_remote(): async def _create_remote():
nonlocal cnt, remote nonlocal cnt
create_remote(remote, TestType.FWD_RUNTIME_ERROR, True) create_remote(remote, TestType.FWD_RUNTIME_ERROR, True)
cnt += 1 cnt += 1
@@ -525,7 +535,7 @@ async def test_forward_runtime_error3():
cnt = 0 cnt = 0
async def _create_remote(): async def _create_remote():
nonlocal cnt, remote nonlocal cnt
create_remote(remote, TestType.FWD_RUNTIME_ERROR_NO_STREAM, True) create_remote(remote, TestType.FWD_RUNTIME_ERROR_NO_STREAM, True)
cnt += 1 cnt += 1
@@ -543,7 +553,7 @@ async def test_forward_resp():
cnt = 0 cnt = 0
def _close_cb(): def _close_cb():
nonlocal cnt, remote, ifc nonlocal cnt
cnt += 1 cnt += 1
cnt = 0 cnt = 0
@@ -559,9 +569,8 @@ async def test_forward_resp2():
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
remote = StreamPtr(None) remote = StreamPtr(None)
cnt = 0 cnt = 0
def _close_cb(): def _close_cb():
nonlocal cnt, remote, ifc nonlocal cnt
cnt += 1 cnt += 1
cnt = 0 cnt = 0
@@ -571,3 +580,4 @@ async def test_forward_resp2():
await ifc.client_loop('') await ifc.client_loop('')
assert cnt == 1 assert cnt == 1
del ifc del ifc

View File

@@ -85,7 +85,6 @@ def patch_open_connection():
return FakeReader(), FakeWriter() return FakeReader(), FakeWriter()
def new_open(host: str, port: int): def new_open(host: str, port: int):
global test
if test == MockType.RD_TEST_TIMEOUT: if test == MockType.RD_TEST_TIMEOUT:
raise ConnectionRefusedError raise ConnectionRefusedError
elif test == MockType.RD_TEST_EXCEPT: elif test == MockType.RD_TEST_EXCEPT:
@@ -318,7 +317,7 @@ async def test_remote_conn_to_loopback(config_conn, patch_open_connection):
assert cnt == 0 assert cnt == 0
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_remote_conn_to_None(config_conn, patch_open_connection): async def test_remote_conn_to_none(config_conn, patch_open_connection):
'''check if get_extra_info() return None in case of an error''' '''check if get_extra_info() return None in case of an error'''
_ = config_conn _ = config_conn
_ = patch_open_connection _ = patch_open_connection

View File

@@ -85,7 +85,6 @@ def patch_open_connection():
return FakeReader(), FakeWriter() return FakeReader(), FakeWriter()
def new_open(host: str, port: int): def new_open(host: str, port: int):
global test
if test == MockType.RD_TEST_TIMEOUT: if test == MockType.RD_TEST_TIMEOUT:
raise ConnectionRefusedError raise ConnectionRefusedError
elif test == MockType.RD_TEST_EXCEPT: elif test == MockType.RD_TEST_EXCEPT:

View File

@@ -84,7 +84,6 @@ def patch_open_connection():
return FakeReader(), FakeWriter() return FakeReader(), FakeWriter()
def new_open(host: str, port: int): def new_open(host: str, port: int):
global test
if test == MockType.RD_TEST_TIMEOUT: if test == MockType.RD_TEST_TIMEOUT:
raise ConnectionRefusedError raise ConnectionRefusedError
elif test == MockType.RD_TEST_EXCEPT: elif test == MockType.RD_TEST_EXCEPT:

View File

@@ -96,7 +96,6 @@ def test_native_client(test_hostname, test_port):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_mqtt_connection(config_mqtt_conn): async def test_mqtt_connection(config_mqtt_conn):
global NO_MOSQUITTO_TEST
if NO_MOSQUITTO_TEST: if NO_MOSQUITTO_TEST:
pytest.skip('skipping, since Mosquitto is not reliable at the moment') pytest.skip('skipping, since Mosquitto is not reliable at the moment')
@@ -122,7 +121,6 @@ async def test_mqtt_connection(config_mqtt_conn):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_ha_reconnect(config_mqtt_conn): async def test_ha_reconnect(config_mqtt_conn):
global NO_MOSQUITTO_TEST
if NO_MOSQUITTO_TEST: if NO_MOSQUITTO_TEST:
pytest.skip('skipping, since Mosquitto is not reliable at the moment') pytest.skip('skipping, since Mosquitto is not reliable at the moment')

View File

@@ -1,3 +1,4 @@
.data.json .data.json
config.yaml config.yaml
apparmor.txt apparmor.txt
README.md

View File

@@ -66,7 +66,7 @@ clean:
# Build the local add-on with a rootfs and config.yaml # Build the local add-on with a rootfs and config.yaml
# The rootfs is needed to build the add-on Docker container # The rootfs is needed to build the add-on Docker container
# #
local_add_on: rootfs $(ADDON_PATH)/config.yaml $(ADDON_PATH)/apparmor.txt local_add_on: rootfs $(ADDON_PATH)/config.yaml $(ADDON_PATH)/apparmor.txt $(ADDON_PATH)/README.md
# collect source files # collect source files
SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\ SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\
@@ -103,6 +103,9 @@ $(ADDON_PATH)/%.yaml: $(TEMPL)/%.jinja $(TEMPL)/.data.json
$(ADDON_PATH)/%.txt: $(TEMPL)/%.jinja $(TEMPL)/.data.json $(ADDON_PATH)/%.txt: $(TEMPL)/%.jinja $(TEMPL)/.data.json
$(JINJA) --strict --format=json $^ -o $@ $(JINJA) --strict --format=json $^ -o $@
$(ADDON_PATH)/%.md: $(TEMPL)/%.jinja $(TEMPL)/.data.json
$(JINJA) --strict --format=json $^ -o $@
# build a common data.json file from STAGE depending source files # build a common data.json file from STAGE depending source files
# don't touch the destination if the checksum of src and dst is equal # don't touch the destination if the checksum of src and dst is equal
$(TEMPL)/.data.json: FORCE $(TEMPL)/.data.json: FORCE
@@ -119,6 +122,7 @@ repro_files = DOCS.md icon.png logo.png translations/de.yaml translations/en.yam
repro_root = CHANGELOG.md LICENSE.md repro_root = CHANGELOG.md LICENSE.md
repro_templates = config.yaml repro_templates = config.yaml
repro_apparmor = apparmor.txt repro_apparmor = apparmor.txt
repro_readme = README.md
repro_subdirs = translations rootfs repro_subdirs = translations rootfs
repro_vers = debug dev rc rel repro_vers = debug dev rc rel
@@ -126,29 +130,34 @@ repro_all_files := $(foreach dir,$(repro_vers), $(foreach file,$(repro_files),$(
repro_root_files := $(foreach dir,$(repro_vers), $(foreach file,$(repro_root),$(INST_BASE)/ha_addon_$(dir)/$(file))) repro_root_files := $(foreach dir,$(repro_vers), $(foreach file,$(repro_root),$(INST_BASE)/ha_addon_$(dir)/$(file)))
repro_all_templates := $(foreach dir,$(repro_vers), $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_$(dir)/$(file))) repro_all_templates := $(foreach dir,$(repro_vers), $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_$(dir)/$(file)))
repro_all_apparmor := $(foreach dir,$(repro_vers), $(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_$(dir)/$(file))) repro_all_apparmor := $(foreach dir,$(repro_vers), $(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_$(dir)/$(file)))
repro_all_readme := $(foreach dir,$(repro_vers), $(foreach file,$(repro_readme),$(INST_BASE)/ha_addon_$(dir)/$(file)))
repro_all_subdirs := $(foreach dir,$(repro_vers), $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_$(dir)/$(file))) repro_all_subdirs := $(foreach dir,$(repro_vers), $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_$(dir)/$(file)))
debug: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_debug/$(file)) \ debug: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_debug/$(file)) \
$(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_debug/$(file)) \ $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_debug/$(file)) \
$(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_debug/$(file)) \ $(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_debug/$(file)) \
$(foreach file,$(repro_readme),$(INST_BASE)/ha_addon_debug/$(file)) \
$(foreach file,$(repro_files),$(INST_BASE)/ha_addon_debug/$(file)) \ $(foreach file,$(repro_files),$(INST_BASE)/ha_addon_debug/$(file)) \
$(foreach file,$(repro_root),$(INST_BASE)/ha_addon_debug/$(file)) $(foreach file,$(repro_root),$(INST_BASE)/ha_addon_debug/$(file))
dev: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_dev/$(file)) \ dev: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_dev/$(file)) \
$(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_dev/$(file)) \ $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_dev/$(file)) \
$(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_dev/$(file)) \ $(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_dev/$(file)) \
$(foreach file,$(repro_readme),$(INST_BASE)/ha_addon_dev/$(file)) \
$(foreach file,$(repro_files),$(INST_BASE)/ha_addon_dev/$(file)) \ $(foreach file,$(repro_files),$(INST_BASE)/ha_addon_dev/$(file)) \
$(foreach file,$(repro_root),$(INST_BASE)/ha_addon_dev/$(file)) $(foreach file,$(repro_root),$(INST_BASE)/ha_addon_dev/$(file))
rc: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_rc/$(file)) \ rc: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_rc/$(file)) \
$(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_rc/$(file)) \ $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_rc/$(file)) \
$(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_rc/$(file)) \ $(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_rc/$(file)) \
$(foreach file,$(repro_readme),$(INST_BASE)/ha_addon_rc/$(file)) \
$(foreach file,$(repro_files),$(INST_BASE)/ha_addon_rc/$(file)) \ $(foreach file,$(repro_files),$(INST_BASE)/ha_addon_rc/$(file)) \
$(foreach file,$(repro_root),$(INST_BASE)/ha_addon_rc/$(file)) $(foreach file,$(repro_root),$(INST_BASE)/ha_addon_rc/$(file))
rel: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_rel/$(file)) \ rel: $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_rel/$(file)) \
$(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_rel/$(file)) \ $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_rel/$(file)) \
$(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_rel/$(file)) \ $(foreach file,$(repro_apparmor),$(INST_BASE)/ha_addon_rel/$(file)) \
$(foreach file,$(repro_readme),$(INST_BASE)/ha_addon_rel/$(file)) \
$(foreach file,$(repro_files),$(INST_BASE)/ha_addon_rel/$(file)) \ $(foreach file,$(repro_files),$(INST_BASE)/ha_addon_rel/$(file)) \
$(foreach file,$(repro_root),$(INST_BASE)/ha_addon_rel/$(file)) $(foreach file,$(repro_root),$(INST_BASE)/ha_addon_rel/$(file))
@@ -161,6 +170,9 @@ $(repro_all_templates) : $(INST_BASE)/ha_addon_%/config.yaml: $(TEMPL)/config.ji
$(repro_all_apparmor) : $(INST_BASE)/ha_addon_%/apparmor.txt: $(TEMPL)/apparmor.jinja $(TEMPL)/%_data.json $(repro_all_apparmor) : $(INST_BASE)/ha_addon_%/apparmor.txt: $(TEMPL)/apparmor.jinja $(TEMPL)/%_data.json
$(JINJA) --strict $< $(filter %.json,$^) -o $@ $(JINJA) --strict $< $(filter %.json,$^) -o $@
$(repro_all_readme) : $(INST_BASE)/ha_addon_%/README.md: $(TEMPL)/README.jinja $(TEMPL)/%_data.json
$(JINJA) --strict $< $(filter %.json,$^) -o $@
$(filter $(INST_BASE)/ha_addon_debug/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_debug/% : ../% $(filter $(INST_BASE)/ha_addon_debug/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_debug/% : ../%
cp $< $@ cp $< $@
$(filter $(INST_BASE)/ha_addon_dev/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_dev/% : ../% $(filter $(INST_BASE)/ha_addon_dev/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_dev/% : ../%

View File

@@ -13,7 +13,7 @@
# 1 Build Base Image # # 1 Build Base Image #
###################### ######################
ARG BUILD_FROM="ghcr.io/hassio-addons/base:17.2.2" ARG BUILD_FROM="ghcr.io/hassio-addons/base:17.2.3"
# hadolint ignore=DL3006 # hadolint ignore=DL3006
FROM $BUILD_FROM AS base FROM $BUILD_FROM AS base

View File

@@ -0,0 +1,21 @@
# Home Assistant Add-on: {{name}}
{{readme_descr}}
## Features
- Supports TSUN GEN3 PLUS inverters: TSOL-MS2000, MS1800 and MS1600
- Supports TSUN GEN3 PLUS batteries: TSOL-DC1000 (from version 0.13)
- Supports TSUN GEN3 inverters: TSOL-MS3000, MS800, MS700, MS600, MS400, MS350 and MS300
- `Home-Assistant` auto-discovery support
- `MODBUS` support via MQTT topics
- `AT-Command` support via MQTT topics (GEN3PLUS only)
- Faster DataUp interval sends measurement data to the MQTT broker every minute
- Self-sufficient island operation without internet
- Security-Features:
- control access via `AT-commands`
## About
This Add-on and the TSUN Proxy is not related to the company TSUN. It is a private initiative that aims to connect TSUN inverters and storage systems with an MQTT broker. There is no support and no warranty from TSUN.
{{readme_links}}

View File

@@ -5,5 +5,7 @@
"image": "docker.io/sallius/tsun-gen3-addon", "image": "docker.io/sallius/tsun-gen3-addon",
"slug": "tsun-proxy-debug", "slug": "tsun-proxy-debug",
"advanced": true, "advanced": true,
"stage": "experimental" "stage": "experimental",
"readme_descr": "This is a bleeding-edge version of the `TSUN Proxy` Add-On with debuging enabled by default.\n\nThe versions may be based on different feature branches and therefore the range of functions may change.\n\nIt is intended to be used to simulate special situations/problems and should only be used in consultation with the maintainer.\n\nFor production please use the stable version `TSUN Proxy`. If you are interested in a bleeding edge version, we offer the `TSUN Proxy (dev)` version.",
"readme_links": ""
} }

View File

@@ -5,5 +5,7 @@
"image": "docker.io/sallius/tsun-gen3-addon", "image": "docker.io/sallius/tsun-gen3-addon",
"slug": "tsun-proxy-dev", "slug": "tsun-proxy-dev",
"advanced": false, "advanced": false,
"stage": "experimental" "stage": "experimental",
"readme_descr": "This is a bleeding-edge version of the `TSUN Proxy` Add-On.\n\nThe versions may be based on different feature branches and therefore the range of functions may change.\n\nIt is intended for testing new functions or testing new devices that are to be supported with the next release.\nFor production, please use the stable version 'TSUN Proxy'.",
"readme_links": ""
} }

View File

@@ -6,5 +6,8 @@
"image": "ghcr.io/s-allius/tsun-gen3-addon", "image": "ghcr.io/s-allius/tsun-gen3-addon",
"slug": "tsun-proxy-rc", "slug": "tsun-proxy-rc",
"advanced": true, "advanced": true,
"stage": "experimental" "stage": "experimental",
"readme_descr": "This is a release candidate of the `TSUN Proxy` Add-On.\n\nIt is intended for testing the next release.\nFor production, please use the stable version 'TSUN Proxy'.",
"readme_links": ""
} }

View File

@@ -5,5 +5,7 @@
"image": "ghcr.io/s-allius/tsun-gen3-addon", "image": "ghcr.io/s-allius/tsun-gen3-addon",
"slug": "tsun-proxy", "slug": "tsun-proxy",
"advanced": false, "advanced": false,
"stage": "stable" "stage": "stable",
"readme_dsecr": "Integrates TSUN inverters (e.g. TSOL MS800, MS2000, MS3000) and batteries (TSOL DC1000) into Home Assistant.\n\nIt is based on the [TSUN Proxy][tsunproxy] and enables a reliable connection between TSUN devices and an MQTT broker.\n\nWith the Add-on, you can easily retrieve real-time values such as power, current and daily energy and integrate the inverter into Home Assistant.\nThis works even without an internet connection.\n\nThe optional connection to the TSUN Cloud can be disabled!",
"readme_links": "\n[tsunproxy]: https://github.com/s-allius/tsun-gen3-proxy\n"
} }

View File

@@ -5,13 +5,14 @@ from dotenv import load_dotenv
load_dotenv() load_dotenv()
SOLARMAN_SNR = os.getenv('SOLARMAN_SNR', '00000080') SOLARMAN_INV_SNR = os.getenv('SOLARMAN_INV_SNR', '00000080')
SOLARMAN_DCU_SNR = os.getenv('SOLARMAN_INV_SNR', '00000080')
def get_sn() -> bytes: def get_sn() -> bytes:
return bytes.fromhex(SOLARMAN_SNR) return bytes.fromhex(SOLARMAN_INV_SNR)
def get_dcu_sn() -> bytes: def get_dcu_sn() -> bytes:
return b'\x20\x43\x65\x7b' return bytes.fromhex(SOLARMAN_DCU_SNR)
def get_dcu_no() -> bytes: def get_dcu_no() -> bytes:
return b'4100000000000001' return b'4100000000000001'
@@ -27,7 +28,7 @@ def correct_checksum(buf):
return checksum.to_bytes(length=1) return checksum.to_bytes(length=1)
@pytest.fixture @pytest.fixture
def MsgContactInfo(): # Contact Info message def msg_contact_info(): # Contact Info message
msg = b'\xa5\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00' msg = b'\xa5\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53' msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e' msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
@@ -46,13 +47,13 @@ def MsgContactInfo(): # Contact Info message
return msg return msg
@pytest.fixture @pytest.fixture
def MsgContactResp(): # Contact Response message def msg_contact_resp(): # Contact Response message
msg = b'\xa5\x0a\x00\x10\x11\x01\x01' +get_sn() +b'\x02\x01\x6a\xfd\x8f' msg = b'\xa5\x0a\x00\x10\x11\x01\x01' +get_sn() +b'\x02\x01\x6a\xfd\x8f'
msg += b'\x65\x3c\x00\x00\x00\x75\x15' msg += b'\x65\x3c\x00\x00\x00\x75\x15'
return msg return msg
@pytest.fixture @pytest.fixture
def MsgDataInd(): def msg_data_ind():
msg = b'\xa5\x99\x01\x10\x42\x59\x84' +get_sn() +b'\x01\xb0\x02\x2c\x87' msg = b'\xa5\x99\x01\x10\x42\x59\x84' +get_sn() +b'\x01\xb0\x02\x2c\x87'
msg += b'\x22\x32\xb7\x29\x00\x00\xd6\xcf\xe1\x33\x01\x00\x0c\x05\x00\x00' msg += b'\x22\x32\xb7\x29\x00\x00\xd6\xcf\xe1\x33\x01\x00\x0c\x05\x00\x00'
msg += b'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45' msg += b'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
@@ -86,14 +87,14 @@ def MsgDataInd():
return msg return msg
@pytest.fixture @pytest.fixture
def MsgDataResp(): # Contact Response message def msg_data_rsp(): # Contact Response message
msg = b'\xa5\x0a\x00\x10\x12\x80\x84' +get_sn() +b'\x01\x01\xd1\x96\x04' msg = b'\xa5\x0a\x00\x10\x12\x80\x84' +get_sn() +b'\x01\x01\xd1\x96\x04'
msg += b'\x66\x3c\x00\x00\x00\xed\x15' msg += b'\x66\x3c\x00\x00\x00\xed\x15'
return msg return msg
@pytest.fixture @pytest.fixture
def MsgInvalidInfo(): # Contact Info message wrong start byte def msg_invalid_info(): # Contact Info message wrong start byte
msg = b'\x47\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00' msg = b'\x47\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53' msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e' msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
@@ -169,7 +170,7 @@ def dcu_data_rsp_msg(): # 0x1210
return msg return msg
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def ClientConnection(): def client_connection():
host = 'logger.talent-monitoring.com' host = 'logger.talent-monitoring.com'
port = 10000 port = 10000
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
@@ -178,15 +179,15 @@ def ClientConnection():
yield s yield s
s.close() s.close()
def checkResponse(data, Msg): def check_response(data, msg):
check = bytearray(data) check = bytearray(data)
check[5]= Msg[5] # ignore seq check[5]= msg[5] # ignore seq
check[13:18]= Msg[13:18] # ignore timestamp + first byte of repeat time check[13:18]= msg[13:18] # ignore timestamp + first byte of repeat time
check[21]= Msg[21] # ignore crc check[21]= msg[21] # ignore crc
assert check == Msg assert check == msg
def tempClientConnection(): def tempclient_connection():
host = 'logger.talent-monitoring.com' host = 'logger.talent-monitoring.com'
port = 10000 port = 10000
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
@@ -198,53 +199,53 @@ def tempClientConnection():
def test_open_close(): def test_open_close():
try: try:
for _ in tempClientConnection(): for _ in tempclient_connection():
pass # test generator tempClientConnection() pass # test generator tempclient_connection()
except: except TimeoutError:
assert False assert False
def test_conn_msg(ClientConnection,MsgContactInfo, MsgContactResp): def test_conn_msg(client_connection,msg_contact_info, msg_contact_resp):
s = ClientConnection s = client_connection
try: try:
s.sendall(MsgContactInfo) s.sendall(msg_contact_info)
# time.sleep(2.5) time.sleep(2.5)
data = s.recv(1024) data = s.recv(1024)
except TimeoutError: except TimeoutError:
pass pass
# time.sleep(2.5) # time.sleep(2.5)
checkResponse(data, MsgContactResp) check_response(data, msg_contact_resp)
def test_data_ind(ClientConnection,MsgDataInd, MsgDataResp): def test_data_ind(client_connection,msg_data_ind, msg_data_rsp):
s = ClientConnection s = client_connection
try: try:
s.sendall(MsgDataInd) s.sendall(msg_data_ind)
# time.sleep(2.5) # time.sleep(2.5)
data = s.recv(1024) data = s.recv(1024)
except TimeoutError: except TimeoutError:
pass pass
# time.sleep(2.5) # time.sleep(2.5)
checkResponse(data, MsgDataResp) check_response(data, msg_data_rsp)
def test_inavlid_msg(ClientConnection,MsgInvalidInfo,MsgContactInfo, MsgContactResp): def test_inavlid_msg(client_connection,msg_invalid_info,msg_contact_info, msg_contact_resp):
s = ClientConnection s = client_connection
try: try:
s.sendall(MsgInvalidInfo) s.sendall(msg_invalid_info)
# time.sleep(2.5) # time.sleep(2.5)
data = s.recv(1024) data = s.recv(1024)
except TimeoutError: except TimeoutError:
pass pass
# time.sleep(2.5) # time.sleep(2.5)
try: try:
s.sendall(MsgContactInfo) s.sendall(msg_contact_info)
# time.sleep(2.5) # time.sleep(2.5)
data = s.recv(1024) data = s.recv(1024)
except TimeoutError: except TimeoutError:
pass pass
# time.sleep(2.5) # time.sleep(2.5)
checkResponse(data, MsgContactResp) check_response(data, msg_contact_resp)
def test_dcu_dev(ClientConnection,dcu_dev_ind_msg, dcu_dev_rsp_msg): def test_dcu_dev(client_connection,dcu_dev_ind_msg, dcu_dev_rsp_msg):
s = ClientConnection s = client_connection
try: try:
s.sendall(dcu_dev_ind_msg) s.sendall(dcu_dev_ind_msg)
# time.sleep(2.5) # time.sleep(2.5)
@@ -252,10 +253,10 @@ def test_dcu_dev(ClientConnection,dcu_dev_ind_msg, dcu_dev_rsp_msg):
except TimeoutError: except TimeoutError:
pass pass
# time.sleep(2.5) # time.sleep(2.5)
checkResponse(data, dcu_dev_rsp_msg) check_response(data, dcu_dev_rsp_msg)
def test_dcu_ind(ClientConnection,dcu_data_ind_msg, dcu_data_rsp_msg): def test_dcu_ind(client_connection,dcu_data_ind_msg, dcu_data_rsp_msg):
s = ClientConnection s = client_connection
try: try:
s.sendall(dcu_data_ind_msg) s.sendall(dcu_data_ind_msg)
# time.sleep(2.5) # time.sleep(2.5)
@@ -263,4 +264,4 @@ def test_dcu_ind(ClientConnection,dcu_data_ind_msg, dcu_data_rsp_msg):
except TimeoutError: except TimeoutError:
pass pass
# time.sleep(2.5) # time.sleep(2.5)
checkResponse(data, dcu_data_rsp_msg) check_response(data, dcu_data_rsp_msg)