From 2b8dacb0defbafdf93a0ade5dc0ada6580db9782 Mon Sep 17 00:00:00 2001
From: Stefan Allius <122395479+s-allius@users.noreply.github.com>
Date: Mon, 26 Aug 2024 23:49:23 +0200
Subject: [PATCH 1/3] Dev 0.11 (#175)
* use random IP adresses for unit tests
* Docker: The description ist missing (#171)
Fixes #167
* S allius/issue167 (#172)
* cleanup
* Sonar qube 6 (#174)
* test class ModbusConn
---
app/Dockerfile | 12 +---
app/build.sh | 39 +++--------
app/docker-bake.hcl | 93 ++++++++++++++++++++++++++
app/src/async_stream.py | 7 +-
app/src/gen3plus/connection_g3p.py | 9 ++-
app/src/gen3plus/inverter_g3p.py | 15 +++--
app/src/gen3plus/solarman_v5.py | 2 +-
app/src/inverter.py | 14 ++--
app/src/modbus_tcp.py | 8 ++-
app/tests/test_infos_g3.py | 23 +++----
app/tests/test_infos_g3p.py | 41 +++++++++---
app/tests/test_inverter.py | 90 +++++++++++++++++++++++++
app/tests/test_modbus_tcp.py | 78 ++++++++++++++++++++++
app/tests/test_mqtt.py | 6 +-
app/tests/test_singleton.py | 1 +
app/tests/test_solarman.py | 13 +++-
app/tests/test_talent.py | 2 +-
system_tests/test_tcp_socket.py | 104 ++++++++++++++---------------
18 files changed, 422 insertions(+), 135 deletions(-)
create mode 100644 app/docker-bake.hcl
create mode 100644 app/tests/test_inverter.py
create mode 100644 app/tests/test_modbus_tcp.py
diff --git a/app/Dockerfile b/app/Dockerfile
index 90e8b0e..300c1e0 100644
--- a/app/Dockerfile
+++ b/app/Dockerfile
@@ -62,18 +62,10 @@ RUN python -m pip install --no-cache --no-index /root/wheels/* && \
COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
COPY config .
COPY src .
-RUN echo ${VERSION} > /proxy-version.txt
-RUN date > /build-date.txt
+RUN echo ${VERSION} > /proxy-version.txt \
+ && date > /build-date.txt
EXPOSE 5005 8127 10000
# command to run on container start
ENTRYPOINT ["/root/entrypoint.sh"]
CMD [ "python3", "./server.py" ]
-
-
-LABEL org.opencontainers.image.title="TSUN Gen3 Proxy"
-LABEL org.opencontainers.image.authors="Stefan Allius"
-LABEL org.opencontainers.image.source=https://github.com/s-allius/tsun-gen3-proxy
-LABEL org.opencontainers.image.description='This proxy enables a reliable connection between TSUN third generation inverters (eg. TSOL MS600, MS800, MS2000) and an MQTT broker to integrate the inverter into typical home automations.'
-LABEL org.opencontainers.image.licenses="BSD-3-Clause"
-LABEL org.opencontainers.image.vendor="Stefan Allius"
diff --git a/app/build.sh b/app/build.sh
index 7d66c91..ac1c23c 100755
--- a/app/build.sh
+++ b/app/build.sh
@@ -17,6 +17,7 @@ VERSION="${VERSION:1}"
arr=(${VERSION//./ })
MAJOR=${arr[0]}
IMAGE=tsun-gen3-proxy
+
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
@@ -26,44 +27,22 @@ IMAGE=docker.io/sallius/${IMAGE}
VERSION=${VERSION}+$1
elif [[ $1 == rc ]] || [[ $1 == rel ]] || [[ $1 == preview ]] ;then
IMAGE=ghcr.io/s-allius/${IMAGE}
+echo 'login to ghcr.io'
+echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
else
echo argument missing!
echo try: $0 '[debug|dev|preview|rc|rel]'
exit 1
fi
-if [[ $1 == debug ]] ;then
-BUILD_ENV="dev"
-else
-BUILD_ENV="production"
-fi
-
-BUILD_CMD="buildx build --push --build-arg VERSION=${VERSION} --build-arg environment=${BUILD_ENV} --attest type=provenance,mode=max --attest type=sbom,generator=docker/scout-sbom-indexer:latest"
-ARCH="--platform linux/amd64,linux/arm64,linux/arm/v7"
-LABELS="--label org.opencontainers.image.created=${BUILD_DATE} --label org.opencontainers.image.version=${VERSION} --label org.opencontainers.image.revision=${BRANCH}"
+export IMAGE
+export VERSION
+export BUILD_DATE
+export BRANCH
+export MAJOR
echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE
-if [[ $1 == debug ]];then
-docker ${BUILD_CMD} ${ARCH} ${LABELS} --build-arg "LOG_LVL=DEBUG" -t ${IMAGE}:debug app
-
-elif [[ $1 == dev ]];then
-docker ${BUILD_CMD} ${ARCH} ${LABELS} -t ${IMAGE}:dev app
-
-elif [[ $1 == preview ]];then
-echo 'login to ghcr.io'
-echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
-docker ${BUILD_CMD} ${ARCH} ${LABELS} -t ${IMAGE}:preview -t ${IMAGE}:${VERSION} app
-
-elif [[ $1 == rc ]];then
-echo 'login to ghcr.io'
-echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
-docker ${BUILD_CMD} ${ARCH} ${LABELS} -t ${IMAGE}:rc -t ${IMAGE}:${VERSION} app
-
-elif [[ $1 == rel ]];then
-echo 'login to ghcr.io'
-echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
-docker ${BUILD_CMD} ${ARCH} ${LABELS} --no-cache -t ${IMAGE}:latest -t ${IMAGE}:${MAJOR} -t ${IMAGE}:${VERSION} app
-fi
+docker buildx bake -f app/docker-bake.hcl $1
echo -e "${BLUE} => checking docker-compose.yaml file${NC}"
docker-compose config -q
diff --git a/app/docker-bake.hcl b/app/docker-bake.hcl
new file mode 100644
index 0000000..b6d58d9
--- /dev/null
+++ b/app/docker-bake.hcl
@@ -0,0 +1,93 @@
+variable "IMAGE" {
+ default = "tsun-gen3-proxy"
+}
+variable "VERSION" {
+ default = "0.0.0"
+}
+variable "MAJOR" {
+ default = "0"
+}
+variable "BUILD_DATE" {
+ default = "dev"
+}
+variable "BRANCH" {
+ default = ""
+}
+variable "DESCRIPTION" {
+ default = "This proxy enables a reliable connection between TSUN third generation inverters (eg. TSOL MS600, MS800, MS2000) and an MQTT broker to integrate the inverter into typical home automations."
+}
+
+target "_common" {
+ context = "app"
+ dockerfile = "Dockerfile"
+ args = {
+ VERSION = "${VERSION}"
+ environment = "production"
+ }
+ attest = [
+ "type =provenance,mode=max",
+ "type =sbom,generator=docker/scout-sbom-indexer:latest"
+ ]
+ annotations = [
+ "index:org.opencontainers.image.title=TSUN Gen3 Proxy",
+ "index:org.opencontainers.image.authors=Stefan Allius",
+ "index:org.opencontainers.image.created=${BUILD_DATE}",
+ "index:org.opencontainers.image.version=${VERSION}",
+ "index:org.opencontainers.image.revision=${BRANCH}",
+ "index:org.opencontainers.image.description=${DESCRIPTION}",
+ "index:org.opencontainers.image.licenses=BSD-3-Clause",
+ "index:org.opencontainers.image.source=https://github.com/s-allius/tsun-gen3-proxy"
+ ]
+ labels = {
+ "org.opencontainers.image.title" = "TSUN Gen3 Proxy"
+ "org.opencontainers.image.authors" = "Stefan Allius"
+ "org.opencontainers.image.created" = "${BUILD_DATE}"
+ "org.opencontainers.image.version" = "${VERSION}"
+ "org.opencontainers.image.revision" = "${BRANCH}"
+ "org.opencontainers.image.description" = "${DESCRIPTION}"
+ "org.opencontainers.image.licenses" = "BSD-3-Clause"
+ "org.opencontainers.image.source" = "https://github.com/s-allius/tsun-gen3-proxy"
+ }
+ output = [
+ "type=image,push=true"
+ ]
+
+ no-cache = false
+ platforms = ["linux/amd64", "linux/arm64", "linux/arm/v7"]
+}
+
+target "_debug" {
+ args = {
+ LOG_LVL = "DEBUG"
+ environment = "dev"
+ }
+}
+target "_prod" {
+ args = {
+ }
+}
+target "debug" {
+ inherits = ["_common", "_debug"]
+ tags = ["${IMAGE}:debug"]
+}
+
+target "dev" {
+ inherits = ["_common"]
+ tags = ["${IMAGE}:dev"]
+}
+
+target "preview" {
+ inherits = ["_common", "_prod"]
+ tags = ["${IMAGE}:dev", "${IMAGE}:${VERSION}"]
+}
+
+target "rc" {
+ inherits = ["_common", "_prod"]
+ tags = ["${IMAGE}:rc", "${IMAGE}:${VERSION}"]
+}
+
+target "rel" {
+ inherits = ["_common", "_prod"]
+ tags = ["${IMAGE}:latest", "${IMAGE}:${MAJOR}", "${IMAGE}:${VERSION}"]
+ no-cache = true
+}
diff --git a/app/src/async_stream.py b/app/src/async_stream.py
index f6b58b5..5892ba2 100644
--- a/app/src/async_stream.py
+++ b/app/src/async_stream.py
@@ -3,10 +3,15 @@ import logging
import traceback
import time
from asyncio import StreamReader, StreamWriter
-from messages import hex_dump_memory, State
from typing import Self
from itertools import count
+if __name__ == "app.src.async_stream":
+ from app.src.messages import hex_dump_memory, State
+else: # pragma: no cover
+ from messages import hex_dump_memory, State
+
+
import gc
logger = logging.getLogger('conn')
diff --git a/app/src/gen3plus/connection_g3p.py b/app/src/gen3plus/connection_g3p.py
index 6591110..dc4eca1 100644
--- a/app/src/gen3plus/connection_g3p.py
+++ b/app/src/gen3plus/connection_g3p.py
@@ -1,7 +1,12 @@
import logging
from asyncio import StreamReader, StreamWriter
-from async_stream import AsyncStream
-from gen3plus.solarman_v5 import SolarmanV5
+
+if __name__ == "app.src.gen3plus.connection_g3p":
+ from app.src.async_stream import AsyncStream
+ from app.src.gen3plus.solarman_v5 import SolarmanV5
+else: # pragma: no cover
+ from async_stream import AsyncStream
+ from gen3plus.solarman_v5 import SolarmanV5
logger = logging.getLogger('conn')
diff --git a/app/src/gen3plus/inverter_g3p.py b/app/src/gen3plus/inverter_g3p.py
index 6c1d6b5..d9bf0f2 100644
--- a/app/src/gen3plus/inverter_g3p.py
+++ b/app/src/gen3plus/inverter_g3p.py
@@ -3,11 +3,18 @@ import traceback
import json
import asyncio
from asyncio import StreamReader, StreamWriter
-from config import Config
-from inverter import Inverter
-from gen3plus.connection_g3p import ConnectionG3P
from aiomqtt import MqttCodeError
-from infos import Infos
+
+if __name__ == "app.src.gen3plus.inverter_g3p":
+ from app.src.config import Config
+ from app.src.inverter import Inverter
+ from app.src.gen3plus.connection_g3p import ConnectionG3P
+ from app.src.infos import Infos
+else: # pragma: no cover
+ from config import Config
+ from inverter import Inverter
+ from gen3plus.connection_g3p import ConnectionG3P
+ from infos import Infos
logger_mqtt = logging.getLogger('mqtt')
diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py
index 0cbdfab..94089f3 100644
--- a/app/src/gen3plus/solarman_v5.py
+++ b/app/src/gen3plus/solarman_v5.py
@@ -172,7 +172,7 @@ class SolarmanV5(Message):
self.db.set_db_def_value(Register.POLLING_INTERVAL,
self.mb_timeout)
self.db.set_db_def_value(Register.HEARTBEAT_INTERVAL,
- 120) # fixme
+ 120)
self.new_data['controller'] = True
self.state = State.up
diff --git a/app/src/inverter.py b/app/src/inverter.py
index cf20c96..996fa0f 100644
--- a/app/src/inverter.py
+++ b/app/src/inverter.py
@@ -1,11 +1,15 @@
import asyncio
import logging
import json
-from config import Config
-from mqtt import Mqtt
-from infos import Infos
+if __name__ == "app.src.inverter":
+ from app.src.config import Config
+ from app.src.mqtt import Mqtt
+ from app.src.infos import Infos
+else: # pragma: no cover
+ from config import Config
+ from mqtt import Mqtt
+ from infos import Infos
-# logger = logging.getLogger('conn')
logger_mqtt = logging.getLogger('mqtt')
@@ -72,7 +76,7 @@ class Inverter():
Infos.new_stat_data[key] = False
@classmethod
- def class_close(cls, loop) -> None:
+ def class_close(cls, loop) -> None: # pragma: no cover
logging.debug('Inverter.class_close')
logging.info('Close MQTT Task')
loop.run_until_complete(cls.mqtt.close())
diff --git a/app/src/modbus_tcp.py b/app/src/modbus_tcp.py
index 5116bc8..a06e38f 100644
--- a/app/src/modbus_tcp.py
+++ b/app/src/modbus_tcp.py
@@ -1,9 +1,13 @@
import logging
import traceback
import asyncio
-from config import Config
-from gen3plus.inverter_g3p import InverterG3P
+if __name__ == "app.src.modbus_tcp":
+ from app.src.config import Config
+ from app.src.gen3plus.inverter_g3p import InverterG3P
+else: # pragma: no cover
+ from config import Config
+ from gen3plus.inverter_g3p import InverterG3P
logger = logging.getLogger('conn')
diff --git a/app/tests/test_infos_g3.py b/app/tests/test_infos_g3.py
index e811d90..37a8076 100644
--- a/app/tests/test_infos_g3.py
+++ b/app/tests/test_infos_g3.py
@@ -325,7 +325,11 @@ def test_build_ha_conf1(contr_data_seq):
assert tests==4
+def test_build_ha_conf2(contr_data_seq):
+ i = InfosG3()
+ i.static_init() # initialize counter
+ tests = 0
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
if id == 'out_power_123':
@@ -344,9 +348,9 @@ def test_build_ha_conf1(contr_data_seq):
assert d_json == json.dumps({"name": "Active Inverter Connections", "stat_t": "tsun/proxy/proxy", "dev_cla": None, "stat_cla": None, "uniq_id": "inv_count_456", "val_tpl": "{{value_json['Inverter_Cnt'] | int}}", "ic": "mdi:counter", "dev": {"name": "Proxy", "sa": "Proxy", "mdl": "proxy", "mf": "Stefan Allius", "sw": "unknown", "ids": ["proxy"]}, "o": {"name": "proxy", "sw": "unknown"}})
tests +=1
- assert tests==5
+ assert tests==1
-def test_build_ha_conf2(contr_data_seq, inv_data_seq, inv_data_seq2):
+def test_build_ha_conf3(contr_data_seq, inv_data_seq, inv_data_seq2):
i = InfosG3()
for key, result in i.parse (contr_data_seq):
pass # side effect in calling i.parse()
@@ -397,12 +401,9 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23})
tests = 0
for key, update in i.parse (inv_data_seq2):
- if key == 'total':
+ if key == 'total' or key == 'env':
assert update == False
tests +=1
- elif key == 'env':
- assert update == False
- tests +=1
assert tests==3
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
@@ -442,12 +443,9 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
tests = 0
for key, update in i.parse (inv_data_seq2_zero):
- if key == 'total':
+ if key == 'total' or key == 'env':
assert update == False
tests +=1
- elif key == 'env':
- assert update == False
- tests +=1
assert tests==3
assert json.dumps(i.db['total']) == json.dumps({})
@@ -456,12 +454,9 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
tests = 0
for key, update in i.parse (inv_data_seq2):
- if key == 'total':
+ if key == 'total' or key == 'env':
assert update == True
tests +=1
- elif key == 'env':
- assert update == True
- tests +=1
assert tests==3
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py
index 21ef570..f7aef4d 100644
--- a/app/tests/test_infos_g3p.py
+++ b/app/tests/test_infos_g3p.py
@@ -1,18 +1,33 @@
# test_with_pytest.py
-import pytest, json, math
+import pytest, json, math, random
from app.src.infos import Register
from app.src.gen3plus.infos_g3p import InfosG3P
from app.src.gen3plus.infos_g3p import RegisterMap
+@pytest.fixture(scope="session")
+def str_test_ip():
+ ip = ".".join(str(random.randint(1, 254)) for _ in range(4))
+ print(f'random_ip: {ip}')
+ return ip
+
+@pytest.fixture(scope="session")
+def bytes_test_ip(str_test_ip):
+ ip = bytes(str.encode(str_test_ip))
+ l = len(ip)
+ if l < 16:
+ ip = ip + bytearray(16-l)
+ print(f'random_ip: {ip}')
+ return ip
+
@pytest.fixture
-def device_data(): # 0x4110 ftype: 0x02
+def device_data(bytes_test_ip): # 0x4110 ftype: 0x02
msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\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'
msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
- msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54\x31\x39\x32\x2e'
- msg += b'\x31\x36\x38\x2e\x38\x30\x2e\x34\x39\x00\x00\x00\x0f\x00\x01\xb0'
+ msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54' + bytes_test_ip
+ msg += b'\x0f\x00\x01\xb0'
msg += b'\x02\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\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\xfe\xfe\x00\x00'
@@ -63,14 +78,14 @@ def test_default_db():
"collector": {"Chip_Type": "IGEN TECH"},
})
-def test_parse_4110(device_data: bytes):
+def test_parse_4110(str_test_ip, device_data: bytes):
i = InfosG3P(client_mode=False)
i.db.clear()
for key, update in i.parse (device_data, 0x41, 2):
pass # side effect is calling generator i.parse()
assert json.dumps(i.db) == json.dumps({
- 'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": "192.168.80.49", "Sensor_List": "02b0"},
+ 'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": str_test_ip, "Sensor_List": "02b0"},
'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "Collector_Fw_Version": "V1.1.00.0B"},
})
@@ -139,7 +154,11 @@ def test_build_ha_conf1():
assert tests==7
+def test_build_ha_conf2():
+ i = InfosG3P(client_mode=False)
+ i.static_init() # initialize counter
+ tests = 0
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
if id == 'out_power_123':
@@ -161,9 +180,9 @@ def test_build_ha_conf1():
assert d_json == json.dumps({"name": "Active Inverter Connections", "stat_t": "tsun/proxy/proxy", "dev_cla": None, "stat_cla": None, "uniq_id": "inv_count_456", "val_tpl": "{{value_json['Inverter_Cnt'] | int}}", "ic": "mdi:counter", "dev": {"name": "Proxy", "sa": "Proxy", "mdl": "proxy", "mf": "Stefan Allius", "sw": "unknown", "ids": ["proxy"]}, "o": {"name": "proxy", "sw": "unknown"}})
tests +=1
- assert tests==8
+ assert tests==1
-def test_build_ha_conf2():
+def test_build_ha_conf3():
i = InfosG3P(client_mode=True)
i.static_init() # initialize counter
@@ -209,7 +228,11 @@ def test_build_ha_conf2():
assert tests==7
+def test_build_ha_conf4():
+ i = InfosG3P(client_mode=True)
+ i.static_init() # initialize counter
+ tests = 0
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
if id == 'out_power_123':
@@ -231,7 +254,7 @@ def test_build_ha_conf2():
assert d_json == json.dumps({"name": "Active Inverter Connections", "stat_t": "tsun/proxy/proxy", "dev_cla": None, "stat_cla": None, "uniq_id": "inv_count_456", "val_tpl": "{{value_json['Inverter_Cnt'] | int}}", "ic": "mdi:counter", "dev": {"name": "Proxy", "sa": "Proxy", "mdl": "proxy", "mf": "Stefan Allius", "sw": "unknown", "ids": ["proxy"]}, "o": {"name": "proxy", "sw": "unknown"}})
tests +=1
- assert tests==8
+ assert tests==1
def test_exception_and_eval(inverter_data: bytes):
diff --git a/app/tests/test_inverter.py b/app/tests/test_inverter.py
new file mode 100644
index 0000000..40b23bf
--- /dev/null
+++ b/app/tests/test_inverter.py
@@ -0,0 +1,90 @@
+# test_with_pytest.py
+import pytest
+import asyncio
+import aiomqtt
+import logging
+
+from mock import patch, Mock
+from app.src.singleton import Singleton
+from app.src.inverter import Inverter
+from app.src.mqtt import Mqtt
+from app.src.gen3plus.solarman_v5 import SolarmanV5
+from app.src.config import Config
+
+
+pytest_plugins = ('pytest_asyncio',)
+
+
+@pytest.fixture(scope="module", autouse=True)
+def module_init():
+ def new_init(cls, cb_mqtt_is_up):
+ cb_mqtt_is_up()
+
+ Singleton._instances.clear()
+ with patch.object(Mqtt, '__init__', new_init):
+ yield
+
+@pytest.fixture(scope="module")
+def test_port():
+ return 1883
+
+@pytest.fixture(scope="module")
+def test_hostname():
+ # if getenv("GITHUB_ACTIONS") == "true":
+ # return 'mqtt'
+ # else:
+ return 'test.mosquitto.org'
+
+@pytest.fixture
+def config_conn(test_hostname, test_port):
+ Config.act_config = {
+ 'mqtt':{
+ 'host': test_hostname,
+ 'port': test_port,
+ 'user': '',
+ 'passwd': ''
+ },
+ 'ha':{
+ 'auto_conf_prefix': 'homeassistant',
+ 'discovery_prefix': 'homeassistant',
+ 'entity_prefix': 'tsun',
+ 'proxy_node_id': 'test_1',
+ 'proxy_unique_id': ''
+ },
+ 'inverters': {
+ 'allow_all': True,
+ "R170000000000001":{
+ 'node_id': 'inv_1'
+ }
+ }
+ }
+
+@pytest.mark.asyncio
+async def test_inverter_cb(config_conn):
+ _ = config_conn
+
+ with patch.object(Inverter, '_cb_mqtt_is_up', wraps=Inverter._cb_mqtt_is_up) as spy:
+ print('call Inverter.class_init')
+ Inverter.class_init()
+ assert 'homeassistant/' == Inverter.discovery_prfx
+ assert 'tsun/' == Inverter.entity_prfx
+ assert 'test_1/' == Inverter.proxy_node_id
+ spy.assert_called_once()
+
+@pytest.mark.asyncio
+async def test_mqtt_is_up(config_conn):
+ _ = config_conn
+
+ with patch.object(Mqtt, 'publish') as spy:
+ Inverter.class_init()
+ await Inverter._cb_mqtt_is_up()
+ spy.assert_called()
+
+@pytest.mark.asyncio
+async def test_mqtt_proxy_statt_invalid(config_conn):
+ _ = config_conn
+
+ with patch.object(Mqtt, 'publish') as spy:
+ Inverter.class_init()
+ await Inverter._async_publ_mqtt_proxy_stat('InValId_kEy')
+ spy.assert_not_called()
diff --git a/app/tests/test_modbus_tcp.py b/app/tests/test_modbus_tcp.py
new file mode 100644
index 0000000..7792894
--- /dev/null
+++ b/app/tests/test_modbus_tcp.py
@@ -0,0 +1,78 @@
+# test_with_pytest.py
+import pytest
+import asyncio
+
+from mock import patch
+from app.src.singleton import Singleton
+from app.src.config import Config
+from app.src.infos import Infos
+from app.src.modbus_tcp import ModbusConn
+
+
+pytest_plugins = ('pytest_asyncio',)
+
+# initialize the proxy statistics
+Infos.static_init()
+
+@pytest.fixture(scope="module", autouse=True)
+def module_init():
+ Singleton._instances.clear()
+ yield
+
+@pytest.fixture(scope="module")
+def test_port():
+ return 1883
+
+@pytest.fixture(scope="module")
+def test_hostname():
+ # if getenv("GITHUB_ACTIONS") == "true":
+ # return 'mqtt'
+ # else:
+ return 'test.mosquitto.org'
+
+@pytest.fixture
+def config_mqtt_conn(test_hostname, test_port):
+ Config.act_config = {'mqtt':{'host': test_hostname, 'port': test_port, 'user': '', 'passwd': ''},
+ 'ha':{'auto_conf_prefix': 'homeassistant','discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun'}
+ }
+
+@pytest.fixture
+def config_no_conn(test_port):
+ Config.act_config = {'mqtt':{'host': "", 'port': test_port, 'user': '', 'passwd': ''},
+ 'ha':{'auto_conf_prefix': 'homeassistant','discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun'}
+ }
+
+class FakeReader():
+ pass
+
+
+class FakeWriter():
+ pass
+
+
+@pytest.fixture
+def patch_open():
+ async def new_conn(conn):
+ await asyncio.sleep(0.01)
+ return FakeReader(), FakeWriter()
+
+ def new_open(host: str, port: int):
+ return new_conn(None)
+
+ with patch.object(asyncio, 'open_connection', new_open) as conn:
+ yield conn
+
+
+@pytest.mark.asyncio
+async def test_modbus_conn(patch_open):
+ _ = patch_open
+ assert Infos.stat['proxy']['Inverter_Cnt'] == 0
+
+ async with ModbusConn('test.local', 1234) as stream:
+ assert stream.node_id == 'G3P'
+ assert stream.addr == ('test.local', 1234)
+ assert type(stream.reader) is FakeReader
+ assert type(stream.writer) is FakeWriter
+ assert Infos.stat['proxy']['Inverter_Cnt'] == 1
+
+ assert Infos.stat['proxy']['Inverter_Cnt'] == 0
diff --git a/app/tests/test_mqtt.py b/app/tests/test_mqtt.py
index 7dea973..1d7c5dd 100644
--- a/app/tests/test_mqtt.py
+++ b/app/tests/test_mqtt.py
@@ -5,6 +5,7 @@ import aiomqtt
import logging
from mock import patch, Mock
+from app.src.singleton import Singleton
from app.src.mqtt import Mqtt
from app.src.modbus import Modbus
from app.src.gen3plus.solarman_v5 import SolarmanV5
@@ -13,7 +14,10 @@ from app.src.config import Config
pytest_plugins = ('pytest_asyncio',)
-
+@pytest.fixture(scope="module", autouse=True)
+def module_init():
+ Singleton._instances.clear()
+ yield
@pytest.fixture(scope="module")
def test_port():
diff --git a/app/tests/test_singleton.py b/app/tests/test_singleton.py
index 2ea82eb..d645e08 100644
--- a/app/tests/test_singleton.py
+++ b/app/tests/test_singleton.py
@@ -7,6 +7,7 @@ class Test(metaclass=Singleton):
pass # is a dummy test class
def test_singleton_metaclass():
+ Singleton._instances.clear()
a = Test()
assert 1 == len(Singleton._instances)
b = Test()
diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py
index c9227bd..521ef9b 100644
--- a/app/tests/test_solarman.py
+++ b/app/tests/test_solarman.py
@@ -3,6 +3,7 @@ import struct
import time
import asyncio
import logging
+import random
from math import isclose
from app.src.gen3plus.solarman_v5 import SolarmanV5
from app.src.config import Config
@@ -148,6 +149,12 @@ def incorrect_checksum(buf):
checksum = (sum(buf[1:])+1) & 0xff
return checksum.to_bytes(length=1)
+@pytest.fixture(scope="session")
+def str_test_ip():
+ ip = ".".join(str(random.randint(1, 254)) for _ in range(4))
+ print(f'random_ip: {ip}')
+ return ip
+
@pytest.fixture
def device_ind_msg(): # 0x4110
msg = b'\xa5\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
@@ -1692,7 +1699,7 @@ async def test_modbus_polling(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp
m.close()
@pytest.mark.asyncio
-async def test_start_client_mode(config_tsun_inv1):
+async def test_start_client_mode(config_tsun_inv1, str_test_ip):
_ = config_tsun_inv1
assert asyncio.get_running_loop()
m = MemoryStream(b'')
@@ -1700,9 +1707,9 @@ async def test_start_client_mode(config_tsun_inv1):
assert m.no_forwarding == False
assert m.mb_timer.tim == None
assert asyncio.get_running_loop() == m.mb_timer.loop
- await m.send_start_cmd(get_sn_int(), '192.168.1.1', m.mb_first_timeout)
+ await m.send_start_cmd(get_sn_int(), str_test_ip, m.mb_first_timeout)
assert m.writer.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x01\x00!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x030\x00\x000J\xde\xf1\x15')
- assert m.db.get_db_value(Register.IP_ADDRESS) == '192.168.1.1'
+ assert m.db.get_db_value(Register.IP_ADDRESS) == str_test_ip
assert isclose(m.db.get_db_value(Register.POLLING_INTERVAL), 0.5)
assert m.db.get_db_value(Register.HEARTBEAT_INTERVAL) == 120
diff --git a/app/tests/test_talent.py b/app/tests/test_talent.py
index 62df532..13f462e 100644
--- a/app/tests/test_talent.py
+++ b/app/tests/test_talent.py
@@ -1150,7 +1150,7 @@ def test_msg_ota_invalid(config_tsun_inv1, msg_ota_invalid):
m.close()
def test_msg_unknown(config_tsun_inv1, msg_unknown):
- config_tsun_inv1
+ _ = config_tsun_inv1
m = MemoryStream(msg_unknown, (0,), False)
m.db.stat['proxy']['Unknown_Msg'] = 0
m.read() # read complete msg, and dispatch msg
diff --git a/system_tests/test_tcp_socket.py b/system_tests/test_tcp_socket.py
index 00ec70f..e663577 100644
--- a/system_tests/test_tcp_socket.py
+++ b/system_tests/test_tcp_socket.py
@@ -13,31 +13,31 @@ def get_invalid_sn():
@pytest.fixture
-def MsgContactInfo(): # Contact Info message
+def msg_contact_info(): # Contact Info message
return b'\x00\x00\x00\x2c\x10'+get_sn()+b'\x91\x00\x08solarhub\x0fsolarhub\x40123456'
@pytest.fixture
-def MsgContactResp(): # Contact Response message
+def msg_contact_resp(): # Contact Response message
return b'\x00\x00\x00\x14\x10'+get_sn()+b'\x91\x00\x01'
@pytest.fixture
-def MsgContactInfo2(): # Contact Info message
+def msg_contact_info2(): # Contact Info message
return b'\x00\x00\x00\x2c\x10'+get_invalid_sn()+b'\x91\x00\x08solarhub\x0fsolarhub\x40123456'
@pytest.fixture
-def MsgContactResp2(): # Contact Response message
+def msg_contact_resp2(): # Contact Response message
return b'\x00\x00\x00\x14\x10'+get_invalid_sn()+b'\x91\x00\x01'
@pytest.fixture
-def MsgTimeStampReq(): # Get Time Request message
+def msg_timestamp_req(): # Get Time Request message
return b'\x00\x00\x00\x13\x10'+get_sn()+b'\x91\x22'
@pytest.fixture
-def MsgTimeStampResp(): # Get Time Resonse message
+def msg_timestamp_resp(): # Get Time Resonse message
return b'\x00\x00\x00\x1b\x10'+get_sn()+b'\x99\x22\x00\x00\x01\x89\xc6\x63\x4d\x80'
@pytest.fixture
-def MsgContollerInd(): # Data indication from the controller
+def msg_controller_ind(): # Data indication from the controller
msg = b'\x00\x00\x01\x2f\x10'+ get_sn() + b'\x91\x71\x0e\x10\x00\x00\x10'+get_sn()
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x55\x50'
msg += b'\x00\x00\x00\x15\x00\x09\x2b\xa8\x54\x10\x52\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x30\x36\x00\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f'
@@ -49,7 +49,7 @@ def MsgContollerInd(): # Data indication from the controller
return msg
@pytest.fixture
-def MsgInvData(): # Data indication from the controller
+def msg_inv_data(): # Data indication from the controller
msg = b'\x00\x00\x00\x8b\x10'+ get_sn() + b'\x91\x04\x01\x90\x00\x01\x10'+get_inv_no()
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x61\x08'
msg += b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
@@ -57,7 +57,7 @@ def MsgInvData(): # Data indication from the controller
return msg
@pytest.fixture
-def MsgInverterInd(): # Data indication from the inverter
+def msg_inverter_ind(): # Data indication from the inverter
msg = b'\x00\x00\x05\x02\x10'+ get_sn() + b'\x91\x04\x01\x90\x00\x01\x10'+get_inv_no()
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x61\x08'
msg += b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00'
@@ -94,7 +94,7 @@ def MsgInverterInd(): # Data indication from the inverter
return msg
@pytest.fixture
-def MsgOtaUpdateReq(): # Over the air update request from talent cloud
+def msg_ota_update_req(): # Over the air update request from talent cloud
msg = b'\x00\x00\x01\x16\x10'+ get_sn() + b'\x70\x13\x01\x02\x76\x35'
msg += b'\x70\x68\x74\x74\x70'
msg += b'\x3a\x2f\x2f\x77\x77\x77\x2e\x74\x61\x6c\x65\x6e\x74\x2d\x6d\x6f'
@@ -117,7 +117,7 @@ def MsgOtaUpdateReq(): # Over the air update request from talent cloud
@pytest.fixture(scope="session")
-def ClientConnection():
+def client_connection():
host = 'logger.talent-monitoring.com'
port = 5005
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
@@ -127,7 +127,7 @@ def ClientConnection():
time.sleep(2.5)
s.close()
-def tempClientConnection():
+def tempclient_connection():
host = 'logger.talent-monitoring.com'
port = 5005
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
@@ -138,25 +138,25 @@ def tempClientConnection():
def test_open_close():
try:
- for s in tempClientConnection():
- pass
- except:
+ for _ in tempclient_connection():
+ pass # test side effect of generator
+ except Exception:
assert False
-def test_send_contact_info1(ClientConnection, MsgContactInfo, MsgContactResp):
- s = ClientConnection
+def test_send_contact_info1(client_connection, msg_contact_info, msg_contact_resp):
+ s = client_connection
try:
- s.sendall(MsgContactInfo)
+ s.sendall(msg_contact_info)
data = s.recv(1024)
except TimeoutError:
pass
- assert data == MsgContactResp
+ assert data == msg_contact_resp
-def test_send_contact_info2(ClientConnection, MsgContactInfo2, MsgContactInfo, MsgContactResp):
- s = ClientConnection
+def test_send_contact_info2(client_connection, msg_contact_info2, msg_contact_info, msg_contact_resp):
+ s = client_connection
try:
- s.sendall(MsgContactInfo2)
+ s.sendall(msg_contact_info2)
data = s.recv(1024)
except TimeoutError:
pass
@@ -164,73 +164,73 @@ def test_send_contact_info2(ClientConnection, MsgContactInfo2, MsgContactInfo, M
assert False
try:
- s.sendall(MsgContactInfo)
+ s.sendall(msg_contact_info)
data = s.recv(1024)
except TimeoutError:
pass
- assert data == MsgContactResp
+ assert data == msg_contact_resp
-def test_send_contact_info3(ClientConnection, MsgContactInfo, MsgContactResp, MsgTimeStampReq):
- s = ClientConnection
+def test_send_contact_info3(client_connection, msg_contact_info, msg_contact_resp, msg_timestamp_req):
+ s = client_connection
try:
- s.sendall(MsgContactInfo)
+ s.sendall(msg_contact_info)
data = s.recv(1024)
except TimeoutError:
pass
- assert data == MsgContactResp
+ assert data == msg_contact_resp
try:
- s.sendall(MsgTimeStampReq)
+ s.sendall(msg_timestamp_req)
data = s.recv(1024)
except TimeoutError:
pass
-def test_send_contact_resp(ClientConnection, MsgContactResp):
- s = ClientConnection
+def test_send_contact_resp(client_connection, msg_contact_resp):
+ s = client_connection
try:
- s.sendall(MsgContactResp)
+ s.sendall(msg_contact_resp)
data = s.recv(1024)
except TimeoutError:
pass
else:
assert data == b''
-def test_send_ctrl_data(ClientConnection, MsgTimeStampReq, MsgTimeStampResp, MsgContollerInd):
- s = ClientConnection
+def test_send_ctrl_data(client_connection, msg_timestamp_req, msg_timestamp_resp, msg_controller_ind):
+ s = client_connection
try:
- s.sendall(MsgTimeStampReq)
- data = s.recv(1024)
+ s.sendall(msg_timestamp_req)
+ _ = s.recv(1024)
except TimeoutError:
pass
# time.sleep(2.5)
- # assert data == MsgTimeStampResp
+ # assert data == msg_timestamp_resp
try:
- s.sendall(MsgContollerInd)
- data = s.recv(1024)
+ s.sendall(msg_controller_ind)
+ _ = s.recv(1024)
except TimeoutError:
pass
-def test_send_inv_data(ClientConnection, MsgTimeStampReq, MsgTimeStampResp, MsgInvData, MsgInverterInd):
- s = ClientConnection
+def test_send_inv_data(client_connection, msg_timestamp_req, msg_timestamp_resp, msg_inv_data, msg_inverter_ind):
+ s = client_connection
try:
- s.sendall(MsgTimeStampReq)
- data = s.recv(1024)
+ s.sendall(msg_timestamp_req)
+ _ = s.recv(1024)
except TimeoutError:
pass
# time.sleep(32.5)
- # assert data == MsgTimeStampResp
+ # assert data == msg_timestamp_resp
try:
- s.sendall(MsgInvData)
- data = s.recv(1024)
- s.sendall(MsgInverterInd)
- data = s.recv(1024)
+ s.sendall(msg_inv_data)
+ _ = s.recv(1024)
+ s.sendall(msg_inverter_ind)
+ _ = s.recv(1024)
except TimeoutError:
pass
-def test_ota_req(ClientConnection, MsgOtaUpdateReq):
- s = ClientConnection
+def test_ota_req(client_connection, msg_ota_update_req):
+ s = client_connection
try:
- s.sendall(MsgOtaUpdateReq)
- data = s.recv(1024)
+ s.sendall(msg_ota_update_req)
+ _ = s.recv(1024)
except TimeoutError:
pass
From 193eea65afca911f43ac1d9bd809376c4e1c995a Mon Sep 17 00:00:00 2001
From: Stefan Allius <122395479+s-allius@users.noreply.github.com>
Date: Tue, 27 Aug 2024 00:24:11 +0200
Subject: [PATCH 2/3] Update README.md (#176)
add SonarCloude shields
---
README.md | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 910ae53..842c6c2 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,13 @@
-
+
+
+
+
+
+
+