Compare commits
1 Commits
s-allius/i
...
renovate/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
271d6709da |
@@ -6,9 +6,4 @@ PRIVAT_CONTAINER_REGISTRY=docker.io/<user>/
|
||||
|
||||
# registry for official container (preview, rc, rel)
|
||||
PUBLIC_CONTAINER_REGISTRY=ghcr.io/<user>/
|
||||
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'
|
||||
PUBLIC_CR_KEY=
|
||||
2
.github/workflows/python-app.yml
vendored
2
.github/workflows/python-app.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --ignore=F821 --show-source --statistics
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# 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/
|
||||
- name: Test with pytest
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
flake8==7.2.0
|
||||
flake8==7.1.2
|
||||
pytest==8.3.5
|
||||
pytest-asyncio==0.26.0
|
||||
pytest-cov==6.1.0
|
||||
|
||||
@@ -84,10 +84,7 @@ async def test_close_cb():
|
||||
return 0.1
|
||||
def closed():
|
||||
nonlocal cnt
|
||||
# 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()
|
||||
nonlocal ifc
|
||||
ifc.close() # clears the closed callback
|
||||
cnt += 1
|
||||
|
||||
@@ -116,6 +113,7 @@ async def test_close_cb():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read():
|
||||
global test
|
||||
assert asyncio.get_running_loop()
|
||||
reader = FakeReader()
|
||||
reader.test = FakeReader.RD_TEST_13_BYTES
|
||||
@@ -126,13 +124,11 @@ async def test_read():
|
||||
return 1
|
||||
def closed():
|
||||
nonlocal cnt
|
||||
# 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()
|
||||
nonlocal ifc
|
||||
ifc.close() # clears the closed callback
|
||||
cnt += 1
|
||||
def app_read():
|
||||
nonlocal ifc
|
||||
ifc.proc_start -= 3
|
||||
return 0.01 # async wait of 0.01
|
||||
cnt = 0
|
||||
@@ -155,6 +151,7 @@ async def test_read():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write():
|
||||
global test
|
||||
assert asyncio.get_running_loop()
|
||||
reader = FakeReader()
|
||||
reader.test = FakeReader.RD_TEST_13_BYTES
|
||||
@@ -165,13 +162,11 @@ async def test_write():
|
||||
return 1
|
||||
def closed():
|
||||
nonlocal cnt
|
||||
# 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()
|
||||
nonlocal ifc
|
||||
ifc.close() # clears the closed callback
|
||||
cnt += 1
|
||||
def app_read():
|
||||
nonlocal ifc
|
||||
ifc.proc_start -= 3
|
||||
return 0.01 # async wait of 0.01
|
||||
|
||||
@@ -208,6 +203,7 @@ async def test_publ_mqtt_cb():
|
||||
return 0.1
|
||||
async def publ_mqtt():
|
||||
nonlocal cnt
|
||||
nonlocal ifc
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
@@ -237,10 +233,7 @@ async def test_create_remote_cb():
|
||||
return 0.1
|
||||
async def create_remote():
|
||||
nonlocal cnt
|
||||
# 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()
|
||||
nonlocal ifc
|
||||
ifc.close() # clears the closed callback
|
||||
cnt += 1
|
||||
|
||||
@@ -262,6 +255,7 @@ async def test_create_remote_cb():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sw_exception():
|
||||
global test
|
||||
assert asyncio.get_running_loop()
|
||||
reader = FakeReader()
|
||||
reader.test = FakeReader.RD_TEST_SW_EXCEPT
|
||||
@@ -272,10 +266,7 @@ async def test_sw_exception():
|
||||
return 1
|
||||
def closed():
|
||||
nonlocal cnt
|
||||
# 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()
|
||||
nonlocal ifc
|
||||
ifc.close() # clears the closed callback
|
||||
cnt += 1
|
||||
cnt = 0
|
||||
@@ -294,6 +285,7 @@ async def test_sw_exception():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_os_error():
|
||||
global test
|
||||
assert asyncio.get_running_loop()
|
||||
reader = FakeReader()
|
||||
reader.test = FakeReader.RD_TEST_OS_ERROR
|
||||
@@ -301,11 +293,12 @@ async def test_os_error():
|
||||
reader.on_recv.set()
|
||||
writer = FakeWriter()
|
||||
cnt = 0
|
||||
|
||||
def timeout():
|
||||
return 1
|
||||
def closed():
|
||||
nonlocal cnt
|
||||
nonlocal ifc
|
||||
ifc.close() # clears the closed callback
|
||||
cnt += 1
|
||||
cnt = 0
|
||||
ifc = AsyncStreamClient(reader, writer, None, closed)
|
||||
@@ -368,13 +361,10 @@ async def test_forward():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt
|
||||
nonlocal cnt, remote, ifc
|
||||
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 ')
|
||||
cnt += 1
|
||||
|
||||
@@ -392,7 +382,7 @@ async def test_forward_with_conn():
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt
|
||||
nonlocal cnt, remote, ifc
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
@@ -427,7 +417,7 @@ async def test_forward_sw_except():
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt
|
||||
nonlocal cnt, remote
|
||||
create_remote(remote, TestType.FWD_SW_EXCPT)
|
||||
cnt += 1
|
||||
|
||||
@@ -445,7 +435,7 @@ async def test_forward_os_error():
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt
|
||||
nonlocal cnt, remote
|
||||
create_remote(remote, TestType.FWD_OS_ERROR)
|
||||
cnt += 1
|
||||
|
||||
@@ -463,7 +453,7 @@ async def test_forward_os_error2():
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt
|
||||
nonlocal cnt, remote
|
||||
create_remote(remote, TestType.FWD_OS_ERROR, True)
|
||||
cnt += 1
|
||||
|
||||
@@ -481,7 +471,7 @@ async def test_forward_os_error3():
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt
|
||||
nonlocal cnt, remote
|
||||
create_remote(remote, TestType.FWD_OS_ERROR_NO_STREAM)
|
||||
cnt += 1
|
||||
|
||||
@@ -499,7 +489,7 @@ async def test_forward_runtime_error():
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt
|
||||
nonlocal cnt, remote
|
||||
create_remote(remote, TestType.FWD_RUNTIME_ERROR)
|
||||
cnt += 1
|
||||
|
||||
@@ -517,7 +507,7 @@ async def test_forward_runtime_error2():
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt
|
||||
nonlocal cnt, remote
|
||||
create_remote(remote, TestType.FWD_RUNTIME_ERROR, True)
|
||||
cnt += 1
|
||||
|
||||
@@ -535,7 +525,7 @@ async def test_forward_runtime_error3():
|
||||
cnt = 0
|
||||
|
||||
async def _create_remote():
|
||||
nonlocal cnt
|
||||
nonlocal cnt, remote
|
||||
create_remote(remote, TestType.FWD_RUNTIME_ERROR_NO_STREAM, True)
|
||||
cnt += 1
|
||||
|
||||
@@ -553,7 +543,7 @@ async def test_forward_resp():
|
||||
cnt = 0
|
||||
|
||||
def _close_cb():
|
||||
nonlocal cnt
|
||||
nonlocal cnt, remote, ifc
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
@@ -569,8 +559,9 @@ async def test_forward_resp2():
|
||||
assert asyncio.get_running_loop()
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
def _close_cb():
|
||||
nonlocal cnt
|
||||
nonlocal cnt, remote, ifc
|
||||
cnt += 1
|
||||
|
||||
cnt = 0
|
||||
@@ -580,4 +571,3 @@ async def test_forward_resp2():
|
||||
await ifc.client_loop('')
|
||||
assert cnt == 1
|
||||
del ifc
|
||||
|
||||
@@ -85,6 +85,7 @@ def patch_open_connection():
|
||||
return FakeReader(), FakeWriter()
|
||||
|
||||
def new_open(host: str, port: int):
|
||||
global test
|
||||
if test == MockType.RD_TEST_TIMEOUT:
|
||||
raise ConnectionRefusedError
|
||||
elif test == MockType.RD_TEST_EXCEPT:
|
||||
@@ -317,7 +318,7 @@ async def test_remote_conn_to_loopback(config_conn, patch_open_connection):
|
||||
assert cnt == 0
|
||||
|
||||
@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'''
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
|
||||
@@ -85,6 +85,7 @@ def patch_open_connection():
|
||||
return FakeReader(), FakeWriter()
|
||||
|
||||
def new_open(host: str, port: int):
|
||||
global test
|
||||
if test == MockType.RD_TEST_TIMEOUT:
|
||||
raise ConnectionRefusedError
|
||||
elif test == MockType.RD_TEST_EXCEPT:
|
||||
|
||||
@@ -84,6 +84,7 @@ def patch_open_connection():
|
||||
return FakeReader(), FakeWriter()
|
||||
|
||||
def new_open(host: str, port: int):
|
||||
global test
|
||||
if test == MockType.RD_TEST_TIMEOUT:
|
||||
raise ConnectionRefusedError
|
||||
elif test == MockType.RD_TEST_EXCEPT:
|
||||
|
||||
@@ -96,6 +96,7 @@ def test_native_client(test_hostname, test_port):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_connection(config_mqtt_conn):
|
||||
global NO_MOSQUITTO_TEST
|
||||
if NO_MOSQUITTO_TEST:
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
|
||||
@@ -121,6 +122,7 @@ async def test_mqtt_connection(config_mqtt_conn):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ha_reconnect(config_mqtt_conn):
|
||||
global NO_MOSQUITTO_TEST
|
||||
if NO_MOSQUITTO_TEST:
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
|
||||
|
||||
3
ha_addons/.gitignore
vendored
3
ha_addons/.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
.data.json
|
||||
config.yaml
|
||||
apparmor.txt
|
||||
README.md
|
||||
apparmor.txt
|
||||
@@ -66,7 +66,7 @@ clean:
|
||||
# Build the local add-on with a rootfs and config.yaml
|
||||
# The rootfs is needed to build the add-on Docker container
|
||||
#
|
||||
local_add_on: rootfs $(ADDON_PATH)/config.yaml $(ADDON_PATH)/apparmor.txt $(ADDON_PATH)/README.md
|
||||
local_add_on: rootfs $(ADDON_PATH)/config.yaml $(ADDON_PATH)/apparmor.txt
|
||||
|
||||
# collect source files
|
||||
SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\
|
||||
@@ -103,9 +103,6 @@ $(ADDON_PATH)/%.yaml: $(TEMPL)/%.jinja $(TEMPL)/.data.json
|
||||
$(ADDON_PATH)/%.txt: $(TEMPL)/%.jinja $(TEMPL)/.data.json
|
||||
$(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
|
||||
# don't touch the destination if the checksum of src and dst is equal
|
||||
$(TEMPL)/.data.json: FORCE
|
||||
@@ -122,7 +119,6 @@ repro_files = DOCS.md icon.png logo.png translations/de.yaml translations/en.yam
|
||||
repro_root = CHANGELOG.md LICENSE.md
|
||||
repro_templates = config.yaml
|
||||
repro_apparmor = apparmor.txt
|
||||
repro_readme = README.md
|
||||
repro_subdirs = translations rootfs
|
||||
repro_vers = debug dev rc rel
|
||||
|
||||
@@ -130,34 +126,29 @@ 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_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_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)))
|
||||
|
||||
debug: $(foreach file,$(repro_subdirs),$(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_readme),$(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))
|
||||
|
||||
dev: $(foreach file,$(repro_subdirs),$(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_readme),$(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))
|
||||
|
||||
rc: $(foreach file,$(repro_subdirs),$(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_readme),$(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))
|
||||
|
||||
rel: $(foreach file,$(repro_subdirs),$(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_readme),$(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))
|
||||
|
||||
@@ -170,9 +161,6 @@ $(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
|
||||
$(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/% : ../%
|
||||
cp $< $@
|
||||
$(filter $(INST_BASE)/ha_addon_dev/%,$(repro_root_files)) : $(INST_BASE)/ha_addon_dev/% : ../%
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# 1 Build Base Image #
|
||||
######################
|
||||
|
||||
ARG BUILD_FROM="ghcr.io/hassio-addons/base:17.2.3"
|
||||
ARG BUILD_FROM="ghcr.io/hassio-addons/base:17.2.2"
|
||||
# hadolint ignore=DL3006
|
||||
FROM $BUILD_FROM AS base
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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}}
|
||||
@@ -5,7 +5,5 @@
|
||||
"image": "docker.io/sallius/tsun-gen3-addon",
|
||||
"slug": "tsun-proxy-debug",
|
||||
"advanced": true,
|
||||
"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": ""
|
||||
"stage": "experimental"
|
||||
}
|
||||
@@ -5,7 +5,5 @@
|
||||
"image": "docker.io/sallius/tsun-gen3-addon",
|
||||
"slug": "tsun-proxy-dev",
|
||||
"advanced": false,
|
||||
"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": ""
|
||||
"stage": "experimental"
|
||||
}
|
||||
@@ -6,8 +6,5 @@
|
||||
"image": "ghcr.io/s-allius/tsun-gen3-addon",
|
||||
"slug": "tsun-proxy-rc",
|
||||
"advanced": true,
|
||||
"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": ""
|
||||
|
||||
"stage": "experimental"
|
||||
}
|
||||
@@ -5,7 +5,5 @@
|
||||
"image": "ghcr.io/s-allius/tsun-gen3-addon",
|
||||
"slug": "tsun-proxy",
|
||||
"advanced": false,
|
||||
"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"
|
||||
"stage": "stable"
|
||||
}
|
||||
@@ -5,14 +5,13 @@ from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
SOLARMAN_INV_SNR = os.getenv('SOLARMAN_INV_SNR', '00000080')
|
||||
SOLARMAN_DCU_SNR = os.getenv('SOLARMAN_INV_SNR', '00000080')
|
||||
SOLARMAN_SNR = os.getenv('SOLARMAN_SNR', '00000080')
|
||||
|
||||
def get_sn() -> bytes:
|
||||
return bytes.fromhex(SOLARMAN_INV_SNR)
|
||||
return bytes.fromhex(SOLARMAN_SNR)
|
||||
|
||||
def get_dcu_sn() -> bytes:
|
||||
return bytes.fromhex(SOLARMAN_DCU_SNR)
|
||||
return b'\x20\x43\x65\x7b'
|
||||
|
||||
def get_dcu_no() -> bytes:
|
||||
return b'4100000000000001'
|
||||
@@ -28,7 +27,7 @@ def correct_checksum(buf):
|
||||
return checksum.to_bytes(length=1)
|
||||
|
||||
@pytest.fixture
|
||||
def msg_contact_info(): # Contact Info message
|
||||
def MsgContactInfo(): # Contact Info message
|
||||
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'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
|
||||
@@ -47,13 +46,13 @@ def msg_contact_info(): # Contact Info message
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def msg_contact_resp(): # Contact Response message
|
||||
def MsgContactResp(): # Contact Response message
|
||||
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'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def msg_data_ind():
|
||||
def MsgDataInd():
|
||||
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'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
|
||||
@@ -87,14 +86,14 @@ def msg_data_ind():
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def msg_data_rsp(): # Contact Response message
|
||||
def MsgDataResp(): # Contact Response message
|
||||
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'
|
||||
return msg
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def msg_invalid_info(): # Contact Info message wrong start byte
|
||||
def MsgInvalidInfo(): # 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'\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'
|
||||
@@ -170,7 +169,7 @@ def dcu_data_rsp_msg(): # 0x1210
|
||||
return msg
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def client_connection():
|
||||
def ClientConnection():
|
||||
host = 'logger.talent-monitoring.com'
|
||||
port = 10000
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
@@ -179,15 +178,15 @@ def client_connection():
|
||||
yield s
|
||||
s.close()
|
||||
|
||||
def check_response(data, msg):
|
||||
def checkResponse(data, Msg):
|
||||
check = bytearray(data)
|
||||
check[5]= msg[5] # ignore seq
|
||||
check[13:18]= msg[13:18] # ignore timestamp + first byte of repeat time
|
||||
check[21]= msg[21] # ignore crc
|
||||
assert check == msg
|
||||
check[5]= Msg[5] # ignore seq
|
||||
check[13:18]= Msg[13:18] # ignore timestamp + first byte of repeat time
|
||||
check[21]= Msg[21] # ignore crc
|
||||
assert check == Msg
|
||||
|
||||
|
||||
def tempclient_connection():
|
||||
def tempClientConnection():
|
||||
host = 'logger.talent-monitoring.com'
|
||||
port = 10000
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
@@ -199,53 +198,53 @@ def tempclient_connection():
|
||||
|
||||
def test_open_close():
|
||||
try:
|
||||
for _ in tempclient_connection():
|
||||
pass # test generator tempclient_connection()
|
||||
except TimeoutError:
|
||||
for _ in tempClientConnection():
|
||||
pass # test generator tempClientConnection()
|
||||
except:
|
||||
assert False
|
||||
|
||||
def test_conn_msg(client_connection,msg_contact_info, msg_contact_resp):
|
||||
s = client_connection
|
||||
def test_conn_msg(ClientConnection,MsgContactInfo, MsgContactResp):
|
||||
s = ClientConnection
|
||||
try:
|
||||
s.sendall(msg_contact_info)
|
||||
time.sleep(2.5)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
# time.sleep(2.5)
|
||||
check_response(data, msg_contact_resp)
|
||||
|
||||
def test_data_ind(client_connection,msg_data_ind, msg_data_rsp):
|
||||
s = client_connection
|
||||
try:
|
||||
s.sendall(msg_data_ind)
|
||||
s.sendall(MsgContactInfo)
|
||||
# time.sleep(2.5)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
# time.sleep(2.5)
|
||||
check_response(data, msg_data_rsp)
|
||||
checkResponse(data, MsgContactResp)
|
||||
|
||||
def test_inavlid_msg(client_connection,msg_invalid_info,msg_contact_info, msg_contact_resp):
|
||||
s = client_connection
|
||||
def test_data_ind(ClientConnection,MsgDataInd, MsgDataResp):
|
||||
s = ClientConnection
|
||||
try:
|
||||
s.sendall(msg_invalid_info)
|
||||
s.sendall(MsgDataInd)
|
||||
# time.sleep(2.5)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
# time.sleep(2.5)
|
||||
checkResponse(data, MsgDataResp)
|
||||
|
||||
def test_inavlid_msg(ClientConnection,MsgInvalidInfo,MsgContactInfo, MsgContactResp):
|
||||
s = ClientConnection
|
||||
try:
|
||||
s.sendall(MsgInvalidInfo)
|
||||
# time.sleep(2.5)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
# time.sleep(2.5)
|
||||
try:
|
||||
s.sendall(msg_contact_info)
|
||||
s.sendall(MsgContactInfo)
|
||||
# time.sleep(2.5)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
# time.sleep(2.5)
|
||||
check_response(data, msg_contact_resp)
|
||||
checkResponse(data, MsgContactResp)
|
||||
|
||||
def test_dcu_dev(client_connection,dcu_dev_ind_msg, dcu_dev_rsp_msg):
|
||||
s = client_connection
|
||||
def test_dcu_dev(ClientConnection,dcu_dev_ind_msg, dcu_dev_rsp_msg):
|
||||
s = ClientConnection
|
||||
try:
|
||||
s.sendall(dcu_dev_ind_msg)
|
||||
# time.sleep(2.5)
|
||||
@@ -253,10 +252,10 @@ def test_dcu_dev(client_connection,dcu_dev_ind_msg, dcu_dev_rsp_msg):
|
||||
except TimeoutError:
|
||||
pass
|
||||
# time.sleep(2.5)
|
||||
check_response(data, dcu_dev_rsp_msg)
|
||||
checkResponse(data, dcu_dev_rsp_msg)
|
||||
|
||||
def test_dcu_ind(client_connection,dcu_data_ind_msg, dcu_data_rsp_msg):
|
||||
s = client_connection
|
||||
def test_dcu_ind(ClientConnection,dcu_data_ind_msg, dcu_data_rsp_msg):
|
||||
s = ClientConnection
|
||||
try:
|
||||
s.sendall(dcu_data_ind_msg)
|
||||
# time.sleep(2.5)
|
||||
@@ -264,4 +263,4 @@ def test_dcu_ind(client_connection,dcu_data_ind_msg, dcu_data_rsp_msg):
|
||||
except TimeoutError:
|
||||
pass
|
||||
# time.sleep(2.5)
|
||||
check_response(data, dcu_data_rsp_msg)
|
||||
checkResponse(data, dcu_data_rsp_msg)
|
||||
|
||||
Reference in New Issue
Block a user