* 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
This commit is contained in:
Stefan Allius
2024-08-26 23:49:23 +02:00
committed by GitHub
parent 5db3fbf495
commit 2b8dacb0de
18 changed files with 422 additions and 135 deletions

View File

@@ -62,18 +62,10 @@ RUN python -m pip install --no-cache --no-index /root/wheels/* && \
COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
COPY config . COPY config .
COPY src . COPY src .
RUN echo ${VERSION} > /proxy-version.txt RUN echo ${VERSION} > /proxy-version.txt \
RUN date > /build-date.txt && date > /build-date.txt
EXPOSE 5005 8127 10000 EXPOSE 5005 8127 10000
# command to run on container start # command to run on container start
ENTRYPOINT ["/root/entrypoint.sh"] ENTRYPOINT ["/root/entrypoint.sh"]
CMD [ "python3", "./server.py" ] 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"

View File

@@ -17,6 +17,7 @@ VERSION="${VERSION:1}"
arr=(${VERSION//./ }) arr=(${VERSION//./ })
MAJOR=${arr[0]} MAJOR=${arr[0]}
IMAGE=tsun-gen3-proxy IMAGE=tsun-gen3-proxy
GREEN='\033[0;32m' GREEN='\033[0;32m'
BLUE='\033[0;34m' BLUE='\033[0;34m'
NC='\033[0m' NC='\033[0m'
@@ -26,44 +27,22 @@ IMAGE=docker.io/sallius/${IMAGE}
VERSION=${VERSION}+$1 VERSION=${VERSION}+$1
elif [[ $1 == rc ]] || [[ $1 == rel ]] || [[ $1 == preview ]] ;then elif [[ $1 == rc ]] || [[ $1 == rel ]] || [[ $1 == preview ]] ;then
IMAGE=ghcr.io/s-allius/${IMAGE} IMAGE=ghcr.io/s-allius/${IMAGE}
echo 'login to ghcr.io'
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
else else
echo argument missing! echo argument missing!
echo try: $0 '[debug|dev|preview|rc|rel]' echo try: $0 '[debug|dev|preview|rc|rel]'
exit 1 exit 1
fi fi
if [[ $1 == debug ]] ;then export IMAGE
BUILD_ENV="dev" export VERSION
else export BUILD_DATE
BUILD_ENV="production" export BRANCH
fi export MAJOR
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}"
echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE
if [[ $1 == debug ]];then docker buildx bake -f app/docker-bake.hcl $1
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
echo -e "${BLUE} => checking docker-compose.yaml file${NC}" echo -e "${BLUE} => checking docker-compose.yaml file${NC}"
docker-compose config -q docker-compose config -q

93
app/docker-bake.hcl Normal file
View File

@@ -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
}

View File

@@ -3,10 +3,15 @@ import logging
import traceback import traceback
import time import time
from asyncio import StreamReader, StreamWriter from asyncio import StreamReader, StreamWriter
from messages import hex_dump_memory, State
from typing import Self from typing import Self
from itertools import count 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 import gc
logger = logging.getLogger('conn') logger = logging.getLogger('conn')

View File

@@ -1,7 +1,12 @@
import logging import logging
from asyncio import StreamReader, StreamWriter 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') logger = logging.getLogger('conn')

View File

@@ -3,11 +3,18 @@ import traceback
import json import json
import asyncio import asyncio
from asyncio import StreamReader, StreamWriter from asyncio import StreamReader, StreamWriter
from config import Config
from inverter import Inverter
from gen3plus.connection_g3p import ConnectionG3P
from aiomqtt import MqttCodeError 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') logger_mqtt = logging.getLogger('mqtt')

View File

@@ -172,7 +172,7 @@ class SolarmanV5(Message):
self.db.set_db_def_value(Register.POLLING_INTERVAL, self.db.set_db_def_value(Register.POLLING_INTERVAL,
self.mb_timeout) self.mb_timeout)
self.db.set_db_def_value(Register.HEARTBEAT_INTERVAL, self.db.set_db_def_value(Register.HEARTBEAT_INTERVAL,
120) # fixme 120)
self.new_data['controller'] = True self.new_data['controller'] = True
self.state = State.up self.state = State.up

View File

@@ -1,11 +1,15 @@
import asyncio import asyncio
import logging import logging
import json import json
from config import Config if __name__ == "app.src.inverter":
from mqtt import Mqtt from app.src.config import Config
from infos import Infos 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') logger_mqtt = logging.getLogger('mqtt')
@@ -72,7 +76,7 @@ class Inverter():
Infos.new_stat_data[key] = False Infos.new_stat_data[key] = False
@classmethod @classmethod
def class_close(cls, loop) -> None: def class_close(cls, loop) -> None: # pragma: no cover
logging.debug('Inverter.class_close') logging.debug('Inverter.class_close')
logging.info('Close MQTT Task') logging.info('Close MQTT Task')
loop.run_until_complete(cls.mqtt.close()) loop.run_until_complete(cls.mqtt.close())

View File

@@ -1,9 +1,13 @@
import logging import logging
import traceback import traceback
import asyncio 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') logger = logging.getLogger('conn')

View File

@@ -325,7 +325,11 @@ def test_build_ha_conf1(contr_data_seq):
assert tests==4 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'): 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': 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"}}) 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 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() i = InfosG3()
for key, result in i.parse (contr_data_seq): for key, result in i.parse (contr_data_seq):
pass # side effect in calling i.parse() 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}) assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23})
tests = 0 tests = 0
for key, update in i.parse (inv_data_seq2): for key, update in i.parse (inv_data_seq2):
if key == 'total': if key == 'total' or key == 'env':
assert update == False assert update == False
tests +=1 tests +=1
elif key == 'env':
assert update == False
tests +=1
assert tests==3 assert tests==3
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36}) 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 tests = 0
for key, update in i.parse (inv_data_seq2_zero): for key, update in i.parse (inv_data_seq2_zero):
if key == 'total': if key == 'total' or key == 'env':
assert update == False assert update == False
tests +=1 tests +=1
elif key == 'env':
assert update == False
tests +=1
assert tests==3 assert tests==3
assert json.dumps(i.db['total']) == json.dumps({}) 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 tests = 0
for key, update in i.parse (inv_data_seq2): for key, update in i.parse (inv_data_seq2):
if key == 'total': if key == 'total' or key == 'env':
assert update == True assert update == True
tests +=1 tests +=1
elif key == 'env':
assert update == True
tests +=1
assert tests==3 assert tests==3
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36}) assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})

View File

@@ -1,18 +1,33 @@
# test_with_pytest.py # test_with_pytest.py
import pytest, json, math import pytest, json, math, random
from app.src.infos import Register from app.src.infos import Register
from app.src.gen3plus.infos_g3p import InfosG3P from app.src.gen3plus.infos_g3p import InfosG3P
from app.src.gen3plus.infos_g3p import RegisterMap 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 @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'\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'\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'
msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 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'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54' + bytes_test_ip
msg += b'\x31\x36\x38\x2e\x38\x30\x2e\x34\x39\x00\x00\x00\x0f\x00\x01\xb0' 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'\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\x00\x00\x00\x00'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\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"}, "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 = InfosG3P(client_mode=False)
i.db.clear() i.db.clear()
for key, update in i.parse (device_data, 0x41, 2): for key, update in i.parse (device_data, 0x41, 2):
pass # side effect is calling generator i.parse() pass # side effect is calling generator i.parse()
assert json.dumps(i.db) == json.dumps({ 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"}, '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 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'): 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': 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"}}) 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 tests +=1
assert tests==8 assert tests==1
def test_build_ha_conf2(): def test_build_ha_conf3():
i = InfosG3P(client_mode=True) i = InfosG3P(client_mode=True)
i.static_init() # initialize counter i.static_init() # initialize counter
@@ -209,7 +228,11 @@ def test_build_ha_conf2():
assert tests==7 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'): 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': 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"}}) 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 tests +=1
assert tests==8 assert tests==1
def test_exception_and_eval(inverter_data: bytes): def test_exception_and_eval(inverter_data: bytes):

View File

@@ -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()

View File

@@ -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

View File

@@ -5,6 +5,7 @@ import aiomqtt
import logging import logging
from mock import patch, Mock from mock import patch, Mock
from app.src.singleton import Singleton
from app.src.mqtt import Mqtt from app.src.mqtt import Mqtt
from app.src.modbus import Modbus from app.src.modbus import Modbus
from app.src.gen3plus.solarman_v5 import SolarmanV5 from app.src.gen3plus.solarman_v5 import SolarmanV5
@@ -13,7 +14,10 @@ from app.src.config import Config
pytest_plugins = ('pytest_asyncio',) pytest_plugins = ('pytest_asyncio',)
@pytest.fixture(scope="module", autouse=True)
def module_init():
Singleton._instances.clear()
yield
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def test_port(): def test_port():

View File

@@ -7,6 +7,7 @@ class Test(metaclass=Singleton):
pass # is a dummy test class pass # is a dummy test class
def test_singleton_metaclass(): def test_singleton_metaclass():
Singleton._instances.clear()
a = Test() a = Test()
assert 1 == len(Singleton._instances) assert 1 == len(Singleton._instances)
b = Test() b = Test()

View File

@@ -3,6 +3,7 @@ import struct
import time import time
import asyncio import asyncio
import logging import logging
import random
from math import isclose from math import isclose
from app.src.gen3plus.solarman_v5 import SolarmanV5 from app.src.gen3plus.solarman_v5 import SolarmanV5
from app.src.config import Config from app.src.config import Config
@@ -148,6 +149,12 @@ def incorrect_checksum(buf):
checksum = (sum(buf[1:])+1) & 0xff checksum = (sum(buf[1:])+1) & 0xff
return checksum.to_bytes(length=1) 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 @pytest.fixture
def device_ind_msg(): # 0x4110 def device_ind_msg(): # 0x4110
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'
@@ -1692,7 +1699,7 @@ async def test_modbus_polling(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp
m.close() m.close()
@pytest.mark.asyncio @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 _ = config_tsun_inv1
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
m = MemoryStream(b'') m = MemoryStream(b'')
@@ -1700,9 +1707,9 @@ async def test_start_client_mode(config_tsun_inv1):
assert m.no_forwarding == False assert m.no_forwarding == False
assert m.mb_timer.tim == None assert m.mb_timer.tim == None
assert asyncio.get_running_loop() == m.mb_timer.loop 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.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 isclose(m.db.get_db_value(Register.POLLING_INTERVAL), 0.5)
assert m.db.get_db_value(Register.HEARTBEAT_INTERVAL) == 120 assert m.db.get_db_value(Register.HEARTBEAT_INTERVAL) == 120

View File

@@ -1150,7 +1150,7 @@ def test_msg_ota_invalid(config_tsun_inv1, msg_ota_invalid):
m.close() m.close()
def test_msg_unknown(config_tsun_inv1, msg_unknown): def test_msg_unknown(config_tsun_inv1, msg_unknown):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_unknown, (0,), False) m = MemoryStream(msg_unknown, (0,), False)
m.db.stat['proxy']['Unknown_Msg'] = 0 m.db.stat['proxy']['Unknown_Msg'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg

View File

@@ -13,31 +13,31 @@ def get_invalid_sn():
@pytest.fixture @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' return b'\x00\x00\x00\x2c\x10'+get_sn()+b'\x91\x00\x08solarhub\x0fsolarhub\x40123456'
@pytest.fixture @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' return b'\x00\x00\x00\x14\x10'+get_sn()+b'\x91\x00\x01'
@pytest.fixture @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' return b'\x00\x00\x00\x2c\x10'+get_invalid_sn()+b'\x91\x00\x08solarhub\x0fsolarhub\x40123456'
@pytest.fixture @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' return b'\x00\x00\x00\x14\x10'+get_invalid_sn()+b'\x91\x00\x01'
@pytest.fixture @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' return b'\x00\x00\x00\x13\x10'+get_sn()+b'\x91\x22'
@pytest.fixture @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' return b'\x00\x00\x00\x1b\x10'+get_sn()+b'\x99\x22\x00\x00\x01\x89\xc6\x63\x4d\x80'
@pytest.fixture @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'\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'\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' 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 return msg
@pytest.fixture @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'\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'\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' 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 return msg
@pytest.fixture @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'\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'\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' 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 return msg
@pytest.fixture @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'\x00\x00\x01\x16\x10'+ get_sn() + b'\x70\x13\x01\x02\x76\x35'
msg += b'\x70\x68\x74\x74\x70' 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' 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") @pytest.fixture(scope="session")
def ClientConnection(): def client_connection():
host = 'logger.talent-monitoring.com' host = 'logger.talent-monitoring.com'
port = 5005 port = 5005
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
@@ -127,7 +127,7 @@ def ClientConnection():
time.sleep(2.5) time.sleep(2.5)
s.close() s.close()
def tempClientConnection(): def tempclient_connection():
host = 'logger.talent-monitoring.com' host = 'logger.talent-monitoring.com'
port = 5005 port = 5005
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
@@ -138,25 +138,25 @@ def tempClientConnection():
def test_open_close(): def test_open_close():
try: try:
for s in tempClientConnection(): for _ in tempclient_connection():
pass pass # test side effect of generator
except: except Exception:
assert False assert False
def test_send_contact_info1(ClientConnection, MsgContactInfo, MsgContactResp): def test_send_contact_info1(client_connection, msg_contact_info, msg_contact_resp):
s = ClientConnection s = client_connection
try: try:
s.sendall(MsgContactInfo) s.sendall(msg_contact_info)
data = s.recv(1024) data = s.recv(1024)
except TimeoutError: except TimeoutError:
pass pass
assert data == MsgContactResp assert data == msg_contact_resp
def test_send_contact_info2(ClientConnection, MsgContactInfo2, MsgContactInfo, MsgContactResp): def test_send_contact_info2(client_connection, msg_contact_info2, msg_contact_info, msg_contact_resp):
s = ClientConnection s = client_connection
try: try:
s.sendall(MsgContactInfo2) s.sendall(msg_contact_info2)
data = s.recv(1024) data = s.recv(1024)
except TimeoutError: except TimeoutError:
pass pass
@@ -164,73 +164,73 @@ def test_send_contact_info2(ClientConnection, MsgContactInfo2, MsgContactInfo, M
assert False assert False
try: try:
s.sendall(MsgContactInfo) s.sendall(msg_contact_info)
data = s.recv(1024) data = s.recv(1024)
except TimeoutError: except TimeoutError:
pass pass
assert data == MsgContactResp assert data == msg_contact_resp
def test_send_contact_info3(ClientConnection, MsgContactInfo, MsgContactResp, MsgTimeStampReq): def test_send_contact_info3(client_connection, msg_contact_info, msg_contact_resp, msg_timestamp_req):
s = ClientConnection s = client_connection
try: try:
s.sendall(MsgContactInfo) s.sendall(msg_contact_info)
data = s.recv(1024) data = s.recv(1024)
except TimeoutError: except TimeoutError:
pass pass
assert data == MsgContactResp assert data == msg_contact_resp
try: try:
s.sendall(MsgTimeStampReq) s.sendall(msg_timestamp_req)
data = s.recv(1024) data = s.recv(1024)
except TimeoutError: except TimeoutError:
pass pass
def test_send_contact_resp(ClientConnection, MsgContactResp): def test_send_contact_resp(client_connection, msg_contact_resp):
s = ClientConnection s = client_connection
try: try:
s.sendall(MsgContactResp) s.sendall(msg_contact_resp)
data = s.recv(1024) data = s.recv(1024)
except TimeoutError: except TimeoutError:
pass pass
else: else:
assert data == b'' assert data == b''
def test_send_ctrl_data(ClientConnection, MsgTimeStampReq, MsgTimeStampResp, MsgContollerInd): def test_send_ctrl_data(client_connection, msg_timestamp_req, msg_timestamp_resp, msg_controller_ind):
s = ClientConnection s = client_connection
try: try:
s.sendall(MsgTimeStampReq) s.sendall(msg_timestamp_req)
data = s.recv(1024) _ = s.recv(1024)
except TimeoutError: except TimeoutError:
pass pass
# time.sleep(2.5) # time.sleep(2.5)
# assert data == MsgTimeStampResp # assert data == msg_timestamp_resp
try: try:
s.sendall(MsgContollerInd) s.sendall(msg_controller_ind)
data = s.recv(1024) _ = s.recv(1024)
except TimeoutError: except TimeoutError:
pass pass
def test_send_inv_data(ClientConnection, MsgTimeStampReq, MsgTimeStampResp, MsgInvData, MsgInverterInd): def test_send_inv_data(client_connection, msg_timestamp_req, msg_timestamp_resp, msg_inv_data, msg_inverter_ind):
s = ClientConnection s = client_connection
try: try:
s.sendall(MsgTimeStampReq) s.sendall(msg_timestamp_req)
data = s.recv(1024) _ = s.recv(1024)
except TimeoutError: except TimeoutError:
pass pass
# time.sleep(32.5) # time.sleep(32.5)
# assert data == MsgTimeStampResp # assert data == msg_timestamp_resp
try: try:
s.sendall(MsgInvData) s.sendall(msg_inv_data)
data = s.recv(1024) _ = s.recv(1024)
s.sendall(MsgInverterInd) s.sendall(msg_inverter_ind)
data = s.recv(1024) _ = s.recv(1024)
except TimeoutError: except TimeoutError:
pass pass
def test_ota_req(ClientConnection, MsgOtaUpdateReq): def test_ota_req(client_connection, msg_ota_update_req):
s = ClientConnection s = client_connection
try: try:
s.sendall(MsgOtaUpdateReq) s.sendall(msg_ota_update_req)
data = s.recv(1024) _ = s.recv(1024)
except TimeoutError: except TimeoutError:
pass pass