Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1df8ae55f0 | ||
|
|
311b36aaf1 | ||
|
|
1642fe5a8a | ||
|
|
2b7e671dfc | ||
|
|
a1930d32ae | ||
|
|
11d7d616fa | ||
|
|
5433e18389 | ||
|
|
9006472264 | ||
|
|
605a269d84 | ||
|
|
93392f49c0 | ||
|
|
587ec3d517 | ||
|
|
b98313ae23 | ||
|
|
dc27da2b56 | ||
|
|
0c4bd2a03a | ||
|
|
ba961fdfd7 | ||
|
|
03aa0c5747 | ||
|
|
10ec949a5b | ||
|
|
909d5ca44a | ||
|
|
335e5d1184 | ||
|
|
583ec0c2a7 | ||
|
|
3c8e7666d4 | ||
|
|
5f821802a5 | ||
|
|
fc10912a12 | ||
|
|
4d5da5a91f | ||
|
|
a2dfb4c1a6 | ||
|
|
3adf968a59 | ||
|
|
89d8cecfb5 | ||
|
|
00f735d0ce | ||
|
|
5c940bb7a2 | ||
|
|
8cb8dea47b | ||
|
|
8edbd7928f | ||
|
|
f0e9c67a06 | ||
|
|
505beff6de | ||
|
|
684e591835 | ||
|
|
8b4a94bfcb | ||
|
|
98dab7db99 | ||
|
|
42ae95fd1c | ||
|
|
9ffd105278 | ||
|
|
97f426269f | ||
|
|
c7bf3f2e44 | ||
|
|
2781bf3a14 | ||
|
|
fcd3fddb19 | ||
|
|
88cdcabd6f | ||
|
|
1f2f359188 | ||
|
|
2dd09288d5 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,5 @@ __pycache__
|
||||
mosquitto/**
|
||||
homeassistant/**
|
||||
tsun_proxy/**
|
||||
system_tests/**
|
||||
Doku/**
|
||||
.DS_Store
|
||||
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.4.1] - 2023-10-20
|
||||
|
||||
- fix issue [#18](https://github.com/s-allius/tsun-gen3-proxy/issues/18)
|
||||
- initialize the proxy statistics
|
||||
- avoid resetting total generation counters
|
||||
|
||||
## [0.4.0] - 2023-10-16
|
||||
|
||||
- fix issue [#8](https://github.com/s-allius/tsun-gen3-proxy/issues/8)
|
||||
- implement [#10](https://github.com/s-allius/tsun-gen3-proxy/issues/10)
|
||||
- fix: don't dispatch ignored messages so that they are not forwarded
|
||||
- add systemtests
|
||||
- fix unit tests, which were broken since version 0.3.0
|
||||
- add proxy device to home assistant
|
||||
- add statistic counter to proxy device
|
||||
- support multiple inverter registration at home assistant
|
||||
|
||||
## [0.3.0] - 2023-10-10
|
||||
|
||||
❗Due to the definition of values for diagnostics, the MQTT devices of controller and inverter should be deleted in the Home Assistant before updating to version '0.3.0'. After the update, these are automatically created again. The measurement data is retained.
|
||||
|
||||
### Changes
|
||||
|
||||
- optimize and reduce logging
|
||||
- switch to pathon 3.12
|
||||
- classify some values for diagnostics
|
||||
|
||||
## [0.2.0] - 2023-10-07
|
||||
|
||||
This version halves the size of the Docker image and reduces the attack surface for security vulnerabilities, by omitting unneeded code. The feature set is exactly the same as the previous release version 0.1.0.
|
||||
|
||||
19
README.md
19
README.md
@@ -6,9 +6,9 @@
|
||||
<p align="center">integration</p>
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/BSD-3-Clause"><img alt="License: BSD-3-Clause" src="https://img.shields.io/badge/License-BSD_3--Clause-green.svg"></a>
|
||||
<a href="https://www.python.org/downloads/release/python-3110/"><img alt="Supported Python versions" src="https://img.shields.io/badge/python-3.11-blue.svg"></a>
|
||||
<a href="https://sbtinstruments.github.io/aiomqtt/introduction.html"><img alt="Supported Python versions" src="https://img.shields.io/badge/aiomqtt-1.2.0-lightblue.svg"></a>
|
||||
<a href="https://toml.io/en/v1.0.0"><img alt="Supported Python versions" src="https://img.shields.io/badge/toml-1.0.0-lightblue.svg"></a>
|
||||
<a href="https://www.python.org/downloads/release/python-3120/"><img alt="Supported Python versions" src="https://img.shields.io/badge/python-3.12-blue.svg"></a>
|
||||
<a href="https://sbtinstruments.github.io/aiomqtt/introduction.html"><img alt="Supported aiomqtt versions" src="https://img.shields.io/badge/aiomqtt-1.2.1-lightblue.svg"></a>
|
||||
<a href="https://toml.io/en/v1.0.0"><img alt="Supported toml versions" src="https://img.shields.io/badge/toml-1.0.0-lightblue.svg"></a>
|
||||
|
||||
</p>
|
||||
|
||||
@@ -16,13 +16,16 @@
|
||||
###
|
||||
# Overview
|
||||
|
||||
The "TSUN Gen3 Micro-Inverter" proxy enables a reliable connection between TSUN third generation inverters and an MQTT broker. With the proxy, you can easily retrieve real-time values such as power, current and daily energy and integrate the inverter into typical home automations. This works even without an internet connection. The optional connection to the TSUN Cloud can be disabled!
|
||||
This proxy enables a reliable connection between TSUN third generation inverters and an MQTT broker. With the proxy, you can easily retrieve real-time values such as power, current and daily energy and integrate the inverter into typical home automations. This works even without an internet connection. The optional connection to the TSUN Cloud can be disabled!
|
||||
|
||||
In detail, the inverter establishes a TCP connection to the TSUN cloud to transmit current measured values every 300 seconds. To be able to forward the measurement data to an MQTT broker, the proxy must be looped into this TCP connection.
|
||||
|
||||
Through this, the inverter then establishes a connection to the proxy and the proxy establishes another connection to the TSUN Cloud. The transmitted data is interpreted by the proxy and then passed on to both the TSUN Cloud and the MQTT broker. The connection to the TSUN Cloud is optional and can be switched off in the configuration (default is on). Then no more data is sent to the Internet, but no more remote updates of firmware and operating parameters (e.g. rated power, grid parameters) are possible.
|
||||
|
||||
By means of `docker` a simple installation and operation is possible. By using `docker-composer`, a complete stack of proxy, `MQTT-brocker` and `home-assistant` can be started easily.
|
||||
By means of `docker` a simple installation and operation is possible. By using `docker-composer`, a complete stack of proxy, `MQTT-brocker` and `home-assistant` can be started easily.
|
||||
###
|
||||
ℹ️ This project is not related to the company TSUN. It is a private initiative that aims to connect TSUN inverters with an MQTT broker. There is no support and no warranty from TSUN.
|
||||
###
|
||||
|
||||
```
|
||||
❗An essential requirement is that the proxy can be looped into the connection
|
||||
@@ -37,11 +40,17 @@ If you use a Pi-hole, you can also store the host entry in the Pi-hole.
|
||||
|
||||
## Features
|
||||
|
||||
- supports TSOL MS300, MS350, MS400, MS600, MS700 and MS800 inverters from TSUN
|
||||
- `MQTT` support
|
||||
- `Home-Assistant` auto-discovery support
|
||||
- Self-sufficient island operation without internet
|
||||
- non-root Docker Container
|
||||
|
||||
## Home Assistant Screenshots
|
||||
|
||||
Here are some screenshots of how the inverter is displayed in the Home Assistant:
|
||||
|
||||
https://github.com/s-allius/tsun-gen3-proxy/wiki/home-assistant#home-assistant-screenshots
|
||||
## Requirements
|
||||
|
||||
- A running Docker engine to host the container
|
||||
|
||||
@@ -4,7 +4,7 @@ ARG GID=1000
|
||||
|
||||
#
|
||||
# first stage for our base image
|
||||
FROM python:3.11-alpine AS base
|
||||
FROM python:3.12-alpine AS base
|
||||
USER root
|
||||
|
||||
RUN apk update && \
|
||||
|
||||
@@ -24,7 +24,7 @@ echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE
|
||||
if [[ $1 == dev ]];then
|
||||
docker build --build-arg "VERSION=${VERSION}" --build-arg "LOG_LVL=DEBUG" --label "org.label-schema.build-date=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" -t ${IMAGE}:latest app
|
||||
elif [[ $1 == rc ]];then
|
||||
docker build --no-cache --build-arg "VERSION=${VERSION}" --label "org.label-schema.build-date=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" -t ${IMAGE}:latest app
|
||||
docker build --build-arg "VERSION=${VERSION}" --label "org.label-schema.build-date=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" -t ${IMAGE}:latest app
|
||||
elif [[ $1 == rel ]];then
|
||||
docker build --no-cache --build-arg "VERSION=${VERSION}" --label "org.label-schema.build-date=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" -t ${IMAGE}:latest -t ${IMAGE}:${MAJOR} -t ${IMAGE}:${VERSION} app
|
||||
docker push ghcr.io/s-allius/tsun-gen3-proxy:latest
|
||||
|
||||
@@ -15,6 +15,8 @@ mqtt.passwd = ''
|
||||
ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
|
||||
ha.discovery_prefix = 'homeassistant' # MQTT prefix for discovery topic
|
||||
ha.entity_prefix = 'tsun' # MQTT topic prefix for publishing inverter values
|
||||
ha.proxy_node_id = 'proxy' # MQTT node id, for the proxy_node_id
|
||||
ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a proxy instance
|
||||
|
||||
# microinverters
|
||||
inverters.allow_all = true # allow inverters, even if we have no inverter mapping
|
||||
|
||||
@@ -14,50 +14,16 @@ class AsyncStream(Message):
|
||||
self.remoteStream = remote_stream
|
||||
self.server_side = server_side
|
||||
self.addr = addr
|
||||
self.unique_id = 0
|
||||
self.node_id = ''
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
'''
|
||||
def set_serial_no(self, serial_no : str):
|
||||
logger.debug(f'SerialNo: {serial_no}')
|
||||
|
||||
if self.unique_id != serial_no:
|
||||
|
||||
inverters = Config.get('inverters')
|
||||
#logger.debug(f'Inverters: {inverters}')
|
||||
|
||||
if serial_no in inverters:
|
||||
logger.debug(f'SerialNo {serial_no} allowed!')
|
||||
inv = inverters[serial_no]
|
||||
self.node_id = inv['node_id']
|
||||
self.sug_area = inv['suggested_area']
|
||||
else:
|
||||
logger.debug(f'SerialNo {serial_no} not known!')
|
||||
self.node_id = ''
|
||||
self.sug_area = ''
|
||||
if not inverters['allow_all']:
|
||||
self.unique_id = None
|
||||
|
||||
logger.warning(f'ignore message from unknow inverter! (SerialNo: {serial_no})')
|
||||
return
|
||||
|
||||
self.unique_id = serial_no
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async def loop(self) -> None:
|
||||
|
||||
while True:
|
||||
try:
|
||||
await self.__async_read()
|
||||
|
||||
if self.id_str:
|
||||
self.set_serial_no(self.id_str.decode("utf-8"))
|
||||
|
||||
|
||||
if self.unique_id:
|
||||
await self.__async_write()
|
||||
await self.__async_forward()
|
||||
@@ -114,7 +80,7 @@ class AsyncStream(Message):
|
||||
await self.async_create_remote() # only implmeneted for server side => syncServerStream
|
||||
|
||||
if self.remoteStream:
|
||||
hex_dump_memory(logging.DEBUG, f'Forward to {self.remoteStream.addr}:', self._forward_buffer, len(self._forward_buffer))
|
||||
hex_dump_memory(logging.INFO, f'Forward to {self.remoteStream.addr}:', self._forward_buffer, len(self._forward_buffer))
|
||||
self.remoteStream.writer.write (self._forward_buffer)
|
||||
await self.remoteStream.writer.drain()
|
||||
self._forward_buffer = bytearray(0)
|
||||
|
||||
@@ -25,7 +25,9 @@ class Config():
|
||||
'ha': {
|
||||
'auto_conf_prefix': Use(str),
|
||||
'discovery_prefix': Use(str),
|
||||
'entity_prefix': Use(str)},
|
||||
'entity_prefix': Use(str),
|
||||
'proxy_node_id': Use(str),
|
||||
'proxy_unique_id': Use(str)},
|
||||
|
||||
'inverters': {
|
||||
'allow_all' : Use(bool),
|
||||
|
||||
181
app/src/infos.py
181
app/src/infos.py
@@ -3,19 +3,38 @@ import struct, json, logging, os
|
||||
|
||||
|
||||
class Infos:
|
||||
stat = {}
|
||||
app_name = os.getenv('SERVICE_NAME', 'proxy')
|
||||
version = os.getenv('VERSION', 'unknown')
|
||||
|
||||
@classmethod
|
||||
def static_init(cls):
|
||||
logging.info('Initialize proxy statistics')
|
||||
# init proxy counter in the class.stat dictionary
|
||||
cls.stat['proxy'] = {}
|
||||
for key in cls.__info_defs:
|
||||
name = cls.__info_defs[key]['name']
|
||||
if name[0]=='proxy':
|
||||
cls.stat['proxy'][name[1]] = 0
|
||||
|
||||
# add values from the environment to the device definition table
|
||||
prxy = cls.__info_devs['proxy']
|
||||
prxy['sw'] = cls.version
|
||||
prxy['mdl'] = cls.app_name
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.db = {}
|
||||
self.app_name = os.getenv('SERVICE_NAME', 'proxy')
|
||||
self.version = os.getenv('VERSION', 'unknown')
|
||||
self.tracer = logging.getLogger('data')
|
||||
|
||||
__info_devs={
|
||||
'controller':{ 'name':'Controller', 'mdl':0x00092f90, 'mf':0x000927c0, 'sw':0x00092ba8},
|
||||
'proxy': {'singleton': True, 'name':'Proxy', 'mf':'Stefan Allius'},
|
||||
'controller':{'via':'proxy', 'name':'Controller', 'mdl':0x00092f90, 'mf':0x000927c0, 'sw':0x00092ba8},
|
||||
'inverter': {'via':'controller', 'name':'Micro Inverter', 'mdl':0x00000032, 'mf':0x00000014, 'sw':0x0000001e},
|
||||
'input_pv1': {'via':'inverter', 'name':'Module PV1'},
|
||||
'input_pv2': {'via':'inverter', 'name':'Module PV2'},
|
||||
'input_pv3': {'via':'inverter', 'name':'Module PV3'},
|
||||
'input_pv4': {'via':'inverter', 'name':'Module PV4'},
|
||||
'input_pv2': {'via':'inverter', 'name':'Module PV2', 'dep':{'reg':0x00095b50, 'gte': 2}},
|
||||
'input_pv3': {'via':'inverter', 'name':'Module PV3', 'dep':{'reg':0x00095b50, 'gte': 3}},
|
||||
'input_pv4': {'via':'inverter', 'name':'Module PV4', 'dep':{'reg':0x00095b50, 'gte': 4}},
|
||||
}
|
||||
|
||||
__info_defs={
|
||||
@@ -25,6 +44,7 @@ class Infos:
|
||||
0x00092f90: {'name':['collector', 'Chip_Model'], 'level': logging.DEBUG, 'unit': ''},
|
||||
0x00095a88: {'name':['collector', 'Trace_URL'], 'level': logging.DEBUG, 'unit': ''},
|
||||
0x00095aec: {'name':['collector', 'Logger_URL'], 'level': logging.DEBUG, 'unit': ''},
|
||||
0x00095b50: {'name':['collector', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''},
|
||||
|
||||
# inverter values used for device registration:
|
||||
0x0000000a: {'name':['inverter', 'Product_Name'], 'level': logging.DEBUG, 'unit': ''},
|
||||
@@ -32,7 +52,13 @@ class Infos:
|
||||
0x0000001e: {'name':['inverter', 'Version'], 'level': logging.INFO, 'unit': ''},
|
||||
0x00000028: {'name':['inverter', 'Serial_Number'], 'level': logging.DEBUG, 'unit': ''},
|
||||
0x00000032: {'name':['inverter', 'Equipment_Model'], 'level': logging.DEBUG, 'unit': ''},
|
||||
|
||||
|
||||
# proxy:
|
||||
0xffffff00: {'name':['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha':{'dev':'proxy', 'dev_cla': None, 'stat_cla': None, 'id':'inv_count_', 'fmt':'| int', 'name': 'Inverter Connection Count', 'icon':'mdi:counter'}},
|
||||
0xffffff01: {'name':['proxy', 'Unknown_SNR'], 'singleton': True, 'ha':{'dev':'proxy', 'dev_cla': None, 'stat_cla': None, 'id':'unknown_snr_', 'fmt':'| int', 'name': 'Unknown Serial No', 'icon':'mdi:counter', 'ent_cat':'diagnostic'}},
|
||||
0xffffff02: {'name':['proxy', 'Unknown_Msg'], 'singleton': True, 'ha':{'dev':'proxy', 'dev_cla': None, 'stat_cla': None, 'id':'unknown_msg_', 'fmt':'| int', 'name': 'Unknown Msg Type', 'icon':'mdi:counter', 'ent_cat':'diagnostic'}},
|
||||
# 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':'| float','name': 'Grid Voltage'}},
|
||||
|
||||
# events
|
||||
0x00000191: {'name':['events', '401_'], 'level': logging.DEBUG, 'unit': ''},
|
||||
0x00000192: {'name':['events', '402_'], 'level': logging.DEBUG, 'unit': ''},
|
||||
@@ -52,42 +78,42 @@ class Infos:
|
||||
0x000001a0: {'name':['events', '416_'], 'level': logging.DEBUG, 'unit': ''},
|
||||
|
||||
# grid measures:
|
||||
0x000003e8: {'name':['grid', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'inverter', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'out_volt_', 'fmt':'| float','name': 'Grid Voltage'}},
|
||||
0x0000044c: {'name':['grid', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'inverter', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'out_cur_', 'fmt':'| float','name': 'Grid Current'}},
|
||||
0x000004b0: {'name':['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha':{'dev':'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id':'out_freq_', 'fmt':'| float','name': 'Grid Frequency'}},
|
||||
0x000003e8: {'name':['grid', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'inverter', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'out_volt_', 'fmt':'| float','name': 'Grid Voltage','ent_cat':'diagnostic'}},
|
||||
0x0000044c: {'name':['grid', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'inverter', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'out_cur_', 'fmt':'| float','name': 'Grid Current','ent_cat':'diagnostic'}},
|
||||
0x000004b0: {'name':['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha':{'dev':'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id':'out_freq_', 'fmt':'| float','name': 'Grid Frequency','ent_cat':'diagnostic'}},
|
||||
0x00000640: {'name':['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha':{'dev':'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'out_power_', 'fmt':'| float','name': 'Power'}},
|
||||
0x000005dc: {'name':['env', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha':{'dev':'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'rated_power_','fmt':'| int', 'name': 'Rated Power'}},
|
||||
0x000005dc: {'name':['env', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha':{'dev':'inverter', 'dev_cla': None, 'stat_cla': None, 'id':'rated_power_', 'fmt':'| int', 'name': 'Rated Power','ent_cat':'diagnostic'}},
|
||||
0x00000514: {'name':['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha':{'dev':'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id':'temp_', 'fmt':'| int','name': 'Temperature'}},
|
||||
|
||||
# input measures:
|
||||
0x000006a4: {'name':['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv1_', 'name': 'Voltage', 'val_tpl' :"{{ (value_json['pv1']['Voltage'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}},
|
||||
0x00000708: {'name':['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv1_', 'name': 'Current', 'val_tpl' :"{{ (value_json['pv1']['Current'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}},
|
||||
0x0000076c: {'name':['input', 'pv1', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha':{'dev':'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv1_','name': 'Power', 'val_tpl' :"{{ (value_json['pv1']['Power'] | float)}}", 'icon':'mdi:gauge'}},
|
||||
0x000007d0: {'name':['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv2_', 'name': 'Voltage', 'val_tpl' :"{{ (value_json['pv2']['Voltage'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}},
|
||||
0x00000834: {'name':['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv2_', 'name': 'Current', 'val_tpl' :"{{ (value_json['pv2']['Current'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}},
|
||||
0x00000898: {'name':['input', 'pv2', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha':{'dev':'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv2_','name': 'Power', 'val_tpl' :"{{ (value_json['pv2']['Power'] | float)}}", 'icon':'mdi:gauge'}},
|
||||
0x000008fc: {'name':['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv3_', 'name': 'Voltage', 'val_tpl' :"{{ (value_json['pv3']['Voltage'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}},
|
||||
0x00000960: {'name':['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv3_', 'name': 'Current', 'val_tpl' :"{{ (value_json['pv3']['Current'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}},
|
||||
0x000009c4: {'name':['input', 'pv3', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha':{'dev':'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv3_','name': 'Power', 'val_tpl' :"{{ (value_json['pv3']['Power'] | float)}}", 'icon':'mdi:gauge'}},
|
||||
0x00000a28: {'name':['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv4_', 'name': 'Voltage', 'val_tpl' :"{{ (value_json['pv4']['Voltage'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}},
|
||||
0x00000a8c: {'name':['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv4_', 'name': 'Current', 'val_tpl' :"{{ (value_json['pv4']['Current'] | float)}}", 'unvisible':1, 'icon':'mdi:gauge'}},
|
||||
0x00000af0: {'name':['input', 'pv4', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha':{'dev':'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv4_','name': 'Power', 'val_tpl' :"{{ (value_json['pv4']['Power'] | float)}}", 'icon':'mdi:gauge'}},
|
||||
0x000006a4: {'name':['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv1_', 'name': 'Voltage', 'val_tpl' :"{{ (value_json['pv1']['Voltage'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}},
|
||||
0x00000708: {'name':['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv1_', 'name': 'Current', 'val_tpl' :"{{ (value_json['pv1']['Current'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}},
|
||||
0x0000076c: {'name':['input', 'pv1', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha':{'dev':'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv1_','name': 'Power', 'val_tpl' :"{{ (value_json['pv1']['Power'] | float)}}"}},
|
||||
0x000007d0: {'name':['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv2_', 'name': 'Voltage', 'val_tpl' :"{{ (value_json['pv2']['Voltage'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}},
|
||||
0x00000834: {'name':['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv2_', 'name': 'Current', 'val_tpl' :"{{ (value_json['pv2']['Current'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}},
|
||||
0x00000898: {'name':['input', 'pv2', 'Power'], 'level': logging.INFO, 'unit': 'W', 'ha':{'dev':'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv2_','name': 'Power', 'val_tpl' :"{{ (value_json['pv2']['Power'] | float)}}"}},
|
||||
0x000008fc: {'name':['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv3_', 'name': 'Voltage', 'val_tpl' :"{{ (value_json['pv3']['Voltage'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}},
|
||||
0x00000960: {'name':['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv3_', 'name': 'Current', 'val_tpl' :"{{ (value_json['pv3']['Current'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}},
|
||||
0x000009c4: {'name':['input', 'pv3', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha':{'dev':'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv3_','name': 'Power', 'val_tpl' :"{{ (value_json['pv3']['Power'] | float)}}"}},
|
||||
0x00000a28: {'name':['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'volt_pv4_', 'name': 'Voltage', 'val_tpl' :"{{ (value_json['pv4']['Voltage'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}},
|
||||
0x00000a8c: {'name':['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha':{'dev':'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id':'cur_pv4_', 'name': 'Current', 'val_tpl' :"{{ (value_json['pv4']['Current'] | float)}}", 'icon':'mdi:gauge','ent_cat':'diagnostic'}},
|
||||
0x00000af0: {'name':['input', 'pv4', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha':{'dev':'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id':'power_pv4_','name': 'Power', 'val_tpl' :"{{ (value_json['pv4']['Power'] | float)}}"}},
|
||||
0x00000c1c: {'name':['input', 'pv1', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id':'daily_gen_pv1_','name': 'Daily Generation', 'val_tpl' :"{{ (value_json['pv1']['Daily_Generation'] | float)}}", 'icon':'mdi:solar-power-variant'}},
|
||||
0x00000c80: {'name':['input', 'pv1', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total', 'id':'total_gen_pv1_','name': 'Total Generation', 'val_tpl' :"{{ (value_json['pv1']['Total_Generation'] | float)}}", 'icon':'mdi:solar-power'}},
|
||||
0x00000c80: {'name':['input', 'pv1', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total', 'id':'total_gen_pv1_','name': 'Total Generation', 'val_tpl' :"{{ (value_json['pv1']['Total_Generation'] | float)}}", 'icon':'mdi:solar-power', 'must_incr':True}},
|
||||
0x00000ce4: {'name':['input', 'pv2', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id':'daily_gen_pv2_','name': 'Daily Generation', 'val_tpl' :"{{ (value_json['pv2']['Daily_Generation'] | float)}}", 'icon':'mdi:solar-power-variant'}},
|
||||
0x00000d48: {'name':['input', 'pv2', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total', 'id':'total_gen_pv2_','name': 'Total Generation', 'val_tpl' :"{{ (value_json['pv2']['Total_Generation'] | float)}}", 'icon':'mdi:solar-power'}},
|
||||
0x00000d48: {'name':['input', 'pv2', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total', 'id':'total_gen_pv2_','name': 'Total Generation', 'val_tpl' :"{{ (value_json['pv2']['Total_Generation'] | float)}}", 'icon':'mdi:solar-power', 'must_incr':True}},
|
||||
0x00000dac: {'name':['input', 'pv3', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id':'daily_gen_pv3_','name': 'Daily Generation', 'val_tpl' :"{{ (value_json['pv3']['Daily_Generation'] | float)}}", 'icon':'mdi:solar-power-variant'}},
|
||||
0x00000e10: {'name':['input', 'pv3', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total', 'id':'total_gen_pv3_','name': 'Total Generation', 'val_tpl' :"{{ (value_json['pv3']['Total_Generation'] | float)}}", 'icon':'mdi:solar-power'}},
|
||||
0x00000e10: {'name':['input', 'pv3', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total', 'id':'total_gen_pv3_','name': 'Total Generation', 'val_tpl' :"{{ (value_json['pv3']['Total_Generation'] | float)}}", 'icon':'mdi:solar-power', 'must_incr':True}},
|
||||
0x00000e74: {'name':['input', 'pv4', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id':'daily_gen_pv4_','name': 'Daily Generation', 'val_tpl' :"{{ (value_json['pv4']['Daily_Generation'] | float)}}", 'icon':'mdi:solar-power-variant'}},
|
||||
0x00000ed8: {'name':['input', 'pv4', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total', 'id':'total_gen_pv4_','name': 'Total Generation', 'val_tpl' :"{{ (value_json['pv4']['Total_Generation'] | float)}}", 'icon':'mdi:solar-power'}},
|
||||
0x00000ed8: {'name':['input', 'pv4', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha':{'dev':'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total', 'id':'total_gen_pv4_','name': 'Total Generation', 'val_tpl' :"{{ (value_json['pv4']['Total_Generation'] | float)}}", 'icon':'mdi:solar-power', 'must_incr':True}},
|
||||
# total:
|
||||
0x00000b54: {'name':['total', 'Daily_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha':{'dev':'inverter', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id':'daily_gen_', 'fmt':'| float','name': 'Daily Generation', 'icon':'mdi:solar-power-variant'}},
|
||||
0x00000bb8: {'name':['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha':{'dev':'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id':'total_gen_', 'fmt':'| float','name': 'Total Generation', 'icon':'mdi:solar-power'}},
|
||||
0x00000bb8: {'name':['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha':{'dev':'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id':'total_gen_', 'fmt':'| float','name': 'Total Generation', 'icon':'mdi:solar-power', 'must_incr':True}},
|
||||
|
||||
# controller:
|
||||
0x000c3500: {'name':['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%' , 'ha':{'dev':'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id':'signal_', 'fmt':'| int', 'name': 'Signal Strength', 'icon':'mdi:wifi'}},
|
||||
0x000c96a8: {'name':['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha':{'dev':'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id':'power_on_time_', 'name': 'Power on Time', 'val_tpl':"{{ (value_json['Power_On_Time'] | float)}}", 'nat_prc':'3'}},
|
||||
0x000cf850: {'name':['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha':{'dev':'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id':'data_up_intval_', 'fmt':'| int', 'name': 'Data Up Interval', 'icon':'mdi:update'}},
|
||||
0x000c96a8: {'name':['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha':{'dev':'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id':'power_on_time_', 'name': 'Power on Time', 'val_tpl':"{{ (value_json['Power_On_Time'] | float)}}", 'nat_prc':'3','ent_cat':'diagnostic'}},
|
||||
0x000cf850: {'name':['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha':{'dev':'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id':'data_up_intval_', 'fmt':'| int', 'name': 'Data Up Interval', 'icon':'mdi:update','ent_cat':'diagnostic'}},
|
||||
|
||||
}
|
||||
|
||||
@@ -112,8 +138,21 @@ class Infos:
|
||||
|
||||
return None # unknwon idx, not in __info_defs
|
||||
|
||||
|
||||
def ha_confs(self, prfx="tsun/garagendach/", snr='123', sug_area =''):
|
||||
def ignore_this_device(self, dep:dict) -> bool:
|
||||
'''Checks the equation in the dep dict
|
||||
|
||||
returns 'False' only if the equation is valid; 'True' in any other case'''
|
||||
if 'reg' in dep:
|
||||
value = self.dev_value(dep['reg'])
|
||||
if not value: return True
|
||||
|
||||
if 'gte' in dep:
|
||||
return not value >= dep['gte']
|
||||
elif 'less_eq' in dep:
|
||||
return not value <= dep['less_eq']
|
||||
return True
|
||||
|
||||
def ha_confs(self, ha_prfx, inv_node_id, inv_snr, proxy_node_id, proxy_unique_id, sug_area =''):
|
||||
'''Generator function yields a json register struct for home-assistant auto configuration and a unique entity string
|
||||
|
||||
arguments:
|
||||
@@ -123,6 +162,13 @@ class Infos:
|
||||
tab = self.__info_defs
|
||||
for key in tab:
|
||||
row = tab[key]
|
||||
if 'singleton' in row and row['singleton']:
|
||||
node_id = proxy_node_id
|
||||
snr = proxy_unique_id
|
||||
else:
|
||||
node_id = inv_node_id
|
||||
snr = inv_snr
|
||||
prfx = ha_prfx + node_id
|
||||
|
||||
#check if we have details for home assistant
|
||||
if 'ha' in row:
|
||||
@@ -146,37 +192,55 @@ class Infos:
|
||||
elif 'fmt' in ha:
|
||||
attr['val_tpl'] = '{{value_json' + f"['{row['name'][-1]}'] {ha['fmt']}" + '}}' # eg. 'val_tpl': "{{ value_json['Output_Power']|float }}"
|
||||
|
||||
if 'unit' in row:
|
||||
# add unit_of_meas only, if status_class isn't none. If status_cla is None we want a number format and not line graph in home assistant.
|
||||
# A unit will change the number format to a line graph
|
||||
if 'unit' in row and attr['stat_cla'] != None:
|
||||
attr['unit_of_meas'] = row['unit'] # optional add a 'unit_of_meas' e.g. 'W'
|
||||
if 'icon' in ha:
|
||||
attr['icon'] = ha['icon'] # optional add an icon for the entity
|
||||
attr['ic'] = ha['icon'] # optional add an icon for the entity
|
||||
if 'nat_prc' in ha:
|
||||
attr['sug_dsp_prc'] = ha['nat_prc'] # optional add the precison of floats
|
||||
|
||||
attr['sug_dsp_prc'] = ha['nat_prc'] # optional add the precison of floats
|
||||
if 'ent_cat' in ha:
|
||||
attr['ent_cat'] = ha['ent_cat'] # diagnostic, config
|
||||
|
||||
# eg. 'dev':{'name':'Microinverter','mdl':'MS-600','ids':["inverter_123"],'mf':'TSUN','sa': 'auf Garagendach'}
|
||||
# attr['dev'] = {'name':'Microinverter','mdl':'MS-600','ids':[f'inverter_{snr}'],'mf':'TSUN','sa': 'auf Garagendach'}
|
||||
if 'dev' in ha:
|
||||
device = self.__info_devs[ha['dev']]
|
||||
|
||||
if 'dep' in device and self.ignore_this_device(device['dep']):
|
||||
continue
|
||||
|
||||
dev = {}
|
||||
|
||||
# the same name fpr 'name' and 'suggested area', so we get dedicated devices in home assistant with short value name and headline
|
||||
if 'name' in device:
|
||||
# the same name for 'name' and 'suggested area', so we get dedicated devices in home assistant with short value name and headline
|
||||
if sug_area == '' or ('singleton' in device and device['singleton']):
|
||||
dev['name'] = device['name']
|
||||
dev['sa'] = device['name']
|
||||
# fixme: we ignore the suggested area, since one area make no sense for multiple devices
|
||||
#else:
|
||||
# dev['name'] = sug_area
|
||||
# dev['sa'] = sug_area
|
||||
|
||||
else:
|
||||
dev['name'] = device['name']+' - '+sug_area
|
||||
dev['sa'] = device['name']+' - '+sug_area
|
||||
|
||||
if 'via' in device: # add the link to the parent device
|
||||
dev['via_device'] = f"{device['via']}_{snr}"
|
||||
via = device['via']
|
||||
if via in self.__info_devs:
|
||||
via_dev = self.__info_devs[via]
|
||||
if 'singleton' in via_dev and via_dev['singleton']:
|
||||
dev['via_device'] = via
|
||||
else:
|
||||
dev['via_device'] = f"{via}_{snr}"
|
||||
|
||||
|
||||
for key in ('mdl','mf', 'sw', 'hw'): # add optional values fpr 'modell', 'manufaturer', 'sw version' and 'hw version'
|
||||
if key in device:
|
||||
data = self.dev_value(device[key])
|
||||
if data is not None: dev[key] = data
|
||||
|
||||
if 'singleton' in device and device['singleton']:
|
||||
dev['ids'] = [f"{ha['dev']}"]
|
||||
else:
|
||||
dev['ids'] = [f"{ha['dev']}_{snr}"]
|
||||
|
||||
dev['ids'] = [f"{ha['dev']}_{snr}"]
|
||||
attr['dev'] = dev
|
||||
|
||||
origin = {}
|
||||
@@ -185,16 +249,27 @@ class Infos:
|
||||
attr['o'] = origin
|
||||
|
||||
|
||||
yield json.dumps (attr), component, attr['uniq_id']
|
||||
|
||||
yield json.dumps (attr), component, node_id, attr['uniq_id']
|
||||
|
||||
|
||||
|
||||
def inc_counter (self, counter:str) -> None:
|
||||
'''inc proxy statistic counter'''
|
||||
dict = self.stat['proxy']
|
||||
dict[counter] += 1
|
||||
|
||||
def dec_counter (self, counter:str) -> None:
|
||||
'''dec proxy statistic counter'''
|
||||
dict = self.stat['proxy']
|
||||
dict[counter] -= 1
|
||||
|
||||
|
||||
def __key_obj(self, id) -> list:
|
||||
d = self.__info_defs.get(id, {'name': None, 'level': logging.DEBUG, 'unit': ''})
|
||||
return d['name'], d['level'], d['unit']
|
||||
if 'ha' in d and 'must_incr' in d['ha']:
|
||||
must_incr = d['ha']['must_incr']
|
||||
else:
|
||||
must_incr = False
|
||||
|
||||
return d['name'], d['level'], d['unit'], must_incr
|
||||
|
||||
|
||||
def parse(self, buf) -> None:
|
||||
@@ -210,7 +285,7 @@ class Infos:
|
||||
info_id = result[0]
|
||||
data_type = result[1]
|
||||
ind += 5
|
||||
keys, level, unit = self.__key_obj(info_id)
|
||||
keys, level, unit, must_incr = self.__key_obj(info_id)
|
||||
|
||||
if data_type==0x54: # 'T' -> Pascal-String
|
||||
str_len = buf[ind]
|
||||
@@ -240,8 +315,8 @@ class Infos:
|
||||
dict = dict[key]
|
||||
name += key + '.'
|
||||
|
||||
update = keys[-1] not in dict or dict[keys[-1]] != result
|
||||
dict[keys[-1]] = result
|
||||
update = keys[-1] not in dict or (not must_incr and dict[keys[-1]] != result) or (must_incr and dict[keys[-1]] < result)
|
||||
if update: dict[keys[-1]] = result
|
||||
name += keys[-1]
|
||||
yield keys[0], update
|
||||
else:
|
||||
|
||||
@@ -4,7 +4,8 @@ from async_stream import AsyncStream
|
||||
from mqtt import Mqtt
|
||||
#import gc
|
||||
|
||||
logger = logging.getLogger('conn')
|
||||
#logger = logging.getLogger('conn')
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
|
||||
|
||||
@@ -13,22 +14,28 @@ class Inverter(AsyncStream):
|
||||
def __init__ (self, reader, writer, addr):
|
||||
super().__init__(reader, writer, addr, None, True)
|
||||
self.mqtt = Mqtt()
|
||||
self.ha_restarts = 0
|
||||
self.ha_restarts = -1
|
||||
ha = Config.get('ha')
|
||||
self.entitiy_prfx = ha['entity_prefix'] + '/'
|
||||
self.entity_prfx = ha['entity_prefix'] + '/'
|
||||
self.discovery_prfx = ha['discovery_prefix'] + '/'
|
||||
self.proxy_node_id = ha['proxy_node_id'] + '/'
|
||||
self.proxy_unique_id = ha['proxy_unique_id']
|
||||
|
||||
|
||||
async def server_loop(self, addr):
|
||||
'''Loop for receiving messages from the inverter (server-side)'''
|
||||
logger.info(f'Accept connection from {addr}')
|
||||
logging.info(f'Accept connection from {addr}')
|
||||
self.inc_counter ('Inverter_Cnt')
|
||||
await self.loop()
|
||||
self.dec_counter ('Inverter_Cnt')
|
||||
logging.info(f'Server loop stopped for {addr}')
|
||||
|
||||
# if the server connection closes, we also have to disconnect the connection to te TSUN cloud
|
||||
if self.remoteStream:
|
||||
logging.debug ("disconnect client connection")
|
||||
self.remoteStream.disc()
|
||||
|
||||
await self.__async_publ_mqtt_packet('proxy')
|
||||
|
||||
async def client_loop(self, addr):
|
||||
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
||||
@@ -66,7 +73,6 @@ class Inverter(AsyncStream):
|
||||
|
||||
async def async_publ_mqtt(self) -> None:
|
||||
'''puplish data to MQTT broker'''
|
||||
db = self.db.db
|
||||
# check if new inverter or collector infos are available or when the home assistant has changed the status back to online
|
||||
if (('inverter' in self.new_data and self.new_data['inverter']) or
|
||||
('collector' in self.new_data and self.new_data['collector']) or
|
||||
@@ -75,18 +81,30 @@ class Inverter(AsyncStream):
|
||||
self.ha_restarts = self.mqtt.ha_restarts
|
||||
|
||||
for key in self.new_data:
|
||||
if self.new_data[key] and key in db:
|
||||
await self.__async_publ_mqtt_packet(key)
|
||||
|
||||
async def __async_publ_mqtt_packet(self, key):
|
||||
db = self.db.db
|
||||
stat = self.db.stat
|
||||
if self.new_data[key]:
|
||||
if key in db:
|
||||
data_json = json.dumps(db[key])
|
||||
logger.info(f'{key}: {data_json}')
|
||||
await self.mqtt.publish(f"{self.entitiy_prfx}{self.node_id}{key}", data_json)
|
||||
self.new_data[key] = False
|
||||
node_id = self.node_id
|
||||
elif key in stat:
|
||||
data_json = json.dumps(stat[key])
|
||||
node_id = self.proxy_node_id
|
||||
else:
|
||||
return
|
||||
logger_mqtt.debug(f'{key}: {data_json}')
|
||||
await self.mqtt.publish(f"{self.entity_prfx}{node_id}{key}", data_json)
|
||||
self.new_data[key] = False
|
||||
|
||||
async def __register_home_assistant(self) -> None:
|
||||
'''register all our topics at home assistant'''
|
||||
try:
|
||||
for data_json, component, id in self.db.ha_confs(self.entitiy_prfx + self.node_id, self.unique_id, self.sug_area):
|
||||
#logger.debug(f'MQTT Register: {data_json}')
|
||||
await self.mqtt.publish(f"{self.discovery_prfx}{component}/{self.node_id}{id}/config", data_json)
|
||||
for data_json, component, node_id, id in self.db.ha_confs(self.entity_prfx, self.node_id, self.unique_id, self.proxy_node_id, self.proxy_unique_id, self.sug_area):
|
||||
logger_mqtt.debug(f"MQTT Register: cmp:'{component}' node_id:'{node_id}' {data_json}")
|
||||
await self.mqtt.publish(f"{self.discovery_prfx}{component}/{node_id}{id}/config", data_json)
|
||||
except Exception:
|
||||
logging.error(
|
||||
f"Inverter: Exception:\n"
|
||||
|
||||
@@ -11,30 +11,32 @@ keys=console_formatter,file_formatter
|
||||
level=DEBUG
|
||||
handlers=console_handler,file_handler_name1
|
||||
|
||||
[logger_mesg]
|
||||
level=DEBUG
|
||||
handlers=console_handler,file_handler_name1,file_handler_name2
|
||||
propagate=0
|
||||
qualname=msg
|
||||
|
||||
[logger_conn]
|
||||
level=DEBUG
|
||||
handlers=console_handler,file_handler_name1,file_handler_name2
|
||||
handlers=console_handler,file_handler_name1
|
||||
propagate=0
|
||||
qualname=conn
|
||||
|
||||
[logger_data]
|
||||
level=DEBUG
|
||||
handlers=file_handler_name1,file_handler_name2
|
||||
propagate=0
|
||||
qualname=data
|
||||
|
||||
[logger_mqtt]
|
||||
level=INFO
|
||||
handlers=console_handler,file_handler_name1
|
||||
propagate=0
|
||||
qualname=mqtt
|
||||
|
||||
[logger_data]
|
||||
level=DEBUG
|
||||
handlers=file_handler_name1
|
||||
propagate=0
|
||||
qualname=data
|
||||
|
||||
|
||||
[logger_mesg]
|
||||
level=DEBUG
|
||||
handlers=file_handler_name2
|
||||
propagate=0
|
||||
qualname=msg
|
||||
|
||||
[logger_tracer]
|
||||
level=INFO
|
||||
handlers=file_handler_name2
|
||||
@@ -60,9 +62,9 @@ args=('log/trace.log', when:='midnight')
|
||||
|
||||
[formatter_console_formatter]
|
||||
format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s'
|
||||
datefmt='%d-%m-%Y %H:%M:%S
|
||||
datefmt='%Y-%m-%d %H:%M:%S
|
||||
|
||||
[formatter_file_formatter]
|
||||
format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s'
|
||||
datefmt='%d-%m-%Y %H:%M:%S
|
||||
datefmt='%Y-%m-%d %H:%M:%S
|
||||
|
||||
|
||||
@@ -80,6 +80,9 @@ class Message(metaclass=IterRegistry):
|
||||
self.header_valid = False
|
||||
self.header_len = 0
|
||||
self.data_len = 0
|
||||
self.unique_id = 0
|
||||
self.node_id = ''
|
||||
self.sug_area = ''
|
||||
self._recv_buffer = b''
|
||||
self._send_buffer = bytearray(0)
|
||||
self._forward_buffer = bytearray(0)
|
||||
@@ -106,7 +109,40 @@ class Message(metaclass=IterRegistry):
|
||||
# so we have to erase self.switch, otherwise this instance can't be
|
||||
# deallocated by the garbage collector ==> we get a memory leak
|
||||
del self.switch
|
||||
|
||||
def inc_counter(self, counter:str) -> None:
|
||||
self.db.inc_counter(counter)
|
||||
self.new_data['proxy'] = True
|
||||
|
||||
def dec_counter(self, counter:str) -> None:
|
||||
self.db.dec_counter(counter)
|
||||
self.new_data['proxy'] = True
|
||||
|
||||
def set_serial_no(self, serial_no : str):
|
||||
|
||||
if self.unique_id == serial_no:
|
||||
logger.debug(f'SerialNo: {serial_no}')
|
||||
else:
|
||||
inverters = Config.get('inverters')
|
||||
#logger.debug(f'Inverters: {inverters}')
|
||||
|
||||
if serial_no in inverters:
|
||||
inv = inverters[serial_no]
|
||||
self.node_id = inv['node_id']
|
||||
self.sug_area = inv['suggested_area']
|
||||
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}')
|
||||
else:
|
||||
self.node_id = ''
|
||||
self.sug_area = ''
|
||||
if 'allow_all' not in inverters or not inverters['allow_all']:
|
||||
self.inc_counter('Unknown_SNR')
|
||||
self.unique_id = None
|
||||
logger.warning(f'ignore message from unknow inverter! (SerialNo: {serial_no})')
|
||||
return
|
||||
logger.debug(f'SerialNo {serial_no} not known but accepted!')
|
||||
|
||||
self.unique_id = serial_no
|
||||
|
||||
|
||||
def read(self) -> None:
|
||||
self._read()
|
||||
@@ -115,6 +151,11 @@ class Message(metaclass=IterRegistry):
|
||||
self.__parse_header(self._recv_buffer, len(self._recv_buffer))
|
||||
|
||||
if self.header_valid and len(self._recv_buffer) >= (self.header_len+self.data_len):
|
||||
hex_dump_memory(logging.INFO, f'Received from {self.addr}:', self._recv_buffer, self.header_len+self.data_len)
|
||||
|
||||
if self.id_str:
|
||||
self.set_serial_no(self.id_str.decode("utf-8"))
|
||||
|
||||
self.__dispatch_msg()
|
||||
self.__flush_recv_msg()
|
||||
return
|
||||
@@ -203,11 +244,12 @@ class Message(metaclass=IterRegistry):
|
||||
|
||||
|
||||
def __dispatch_msg(self) -> None:
|
||||
hex_dump_memory(logging.INFO, f'Received from {self.addr}:', self._recv_buffer, self.header_len+self.data_len)
|
||||
|
||||
fnc = self.switch.get(self.msg_id, self.msg_unknown)
|
||||
logger.info(self.__flow_str(self.server_side, 'rx') + f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}' )
|
||||
fnc()
|
||||
if self.unique_id:
|
||||
logger.info(self.__flow_str(self.server_side, 'rx') + f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}' )
|
||||
fnc()
|
||||
else:
|
||||
logger.info(self.__flow_str(self.server_side, 'drop') + f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}' )
|
||||
|
||||
|
||||
def __flush_recv_msg(self) -> None:
|
||||
@@ -296,6 +338,7 @@ class Message(metaclass=IterRegistry):
|
||||
|
||||
def msg_unknown(self):
|
||||
logger.warning (f"Unknow Msg: ID:{self.msg_id}")
|
||||
self.inc_counter('Unknown_Msg')
|
||||
self.forward(self._recv_buffer, self.header_len+self.data_len)
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from async_stream import AsyncStream
|
||||
from inverter import Inverter
|
||||
from config import Config
|
||||
from mqtt import Mqtt
|
||||
from infos import Infos
|
||||
|
||||
|
||||
async def handle_client(reader, writer):
|
||||
@@ -64,10 +65,13 @@ if __name__ == "__main__":
|
||||
# read config file
|
||||
Config.read()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# call Mqtt singleton to establisch the connection to the mqtt broker
|
||||
mqtt = Mqtt()
|
||||
# initialize the proxy statistics
|
||||
Infos.static_init()
|
||||
|
||||
#
|
||||
# Register some UNIX Signal handler for a gracefully server shutdown on Docker restart and stop
|
||||
|
||||
@@ -18,6 +18,76 @@ def InvDataSeq(): # Data indication from the controller
|
||||
msg += b'\x54\x10\x54\x31\x37\x45\x37\x33\x30\x37\x30\x32\x31\x44\x30\x30\x36\x41\x00\x00\x00\x32\x54\x0a\x54\x53\x4f\x4c\x2d\x4d\x53\x36\x30\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def InvDataSeq2(): # Data indication from the controller
|
||||
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\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00'
|
||||
msg += b'\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53'
|
||||
msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00\x00\x00\x01\xf5\x53\x00\x00\x00\x00'
|
||||
msg += b'\x01\xf6\x53\x00\x00\x00\x00\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00\x00\x00'
|
||||
msg += b'\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00'
|
||||
msg += b'\x00\x00\x02\x02\x53\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00\x00\x00\x02\x5a'
|
||||
msg += b'\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02'
|
||||
msg += b'\x60\x53\x00\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02\x65\x53\x00\x00\x00\x00'
|
||||
msg += b'\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00'
|
||||
msg += b'\x00\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3\x53\x00\x00\x00\x00\x02\xc4\x53'
|
||||
msg += b'\x00\x00\x00\x00\x02\xc5\x53\x00\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02\xca'
|
||||
msg += b'\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53\x00\x00\x00\x00\x03\x20\x53\x00\x00\x00\x00\x03\x84\x53\x50\x11\x00\x00\x03\xe8\x46\x43\x61\x66\x66\x00'
|
||||
msg += b'\x00\x04\x4c\x46\x3e\xeb\x85\x1f\x00\x00\x04\xb0\x46\x42\x48\x14\x7b\x00\x00\x05\x14\x53\x00\x17\x00\x00\x05\x78\x53\x00\x00\x00\x00\x05\xdc\x53\x02\x58\x00\x00\x06'
|
||||
msg += b'\x40\x46\x42\xd3\x66\x66\x00\x00\x06\xa4\x46\x42\x06\x66\x66\x00\x00\x07\x08\x46\x3f\xf4\x7a\xe1\x00\x00\x07\x6c\x46\x42\x81\x00\x00\x00\x00\x07\xd0\x46\x42\x06\x00'
|
||||
msg += b'\x00\x00\x00\x08\x34\x46\x3f\xae\x14\x7b\x00\x00\x08\x98\x46\x42\x36\xcc\xcd\x00\x00\x08\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x00\x00\x00\x00\x00\x00\x09\xc4'
|
||||
msg += b'\x46\x00\x00\x00\x00\x00\x00\x0a\x28\x46\x00\x00\x00\x00\x00\x00\x0a\x8c\x46\x00\x00\x00\x00\x00\x00\x0a\xf0\x46\x00\x00\x00\x00\x00\x00\x0b\x54\x46\x3f\xd9\x99\x9a'
|
||||
msg += b'\x00\x00\x0b\xb8\x46\x41\x8a\xe1\x48\x00\x00\x0c\x1c\x46\x3f\x8a\x3d\x71\x00\x00\x0c\x80\x46\x41\x1b\xd7\x0a\x00\x00\x0c\xe4\x46\x3f\x1e\xb8\x52\x00\x00\x0d\x48\x46'
|
||||
msg += b'\x40\xf3\xd7\x0a\x00\x00\x0d\xac\x46\x00\x00\x00\x00\x00\x00\x0e\x10\x46\x00\x00\x00\x00\x00\x00\x0e\x74\x46\x00\x00\x00\x00\x00\x00\x0e\xd8\x46\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0\x53\x00\x00\x00\x00\x10\x04\x53\x55\xaa\x00\x00\x10\x68\x53\x00\x00\x00\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00'
|
||||
msg += b'\x00\x00\x11\x94\x53\x00\x00\x00\x00\x11\xf8\x53\xff\xff\x00\x00\x12\x5c\x53\xff\xff\x00\x00\x12\xc0\x53\x00\x02\x00\x00\x13\x24\x53\xff\xff\x00\x00\x13\x88\x53\xff'
|
||||
msg += b'\xff\x00\x00\x13\xec\x53\xff\xff\x00\x00\x14\x50\x53\xff\xff\x00\x00\x14\xb4\x53\xff\xff\x00\x00\x15\x18\x53\xff\xff\x00\x00\x15\x7c\x53\x00\x00\x00\x00\x27\x10\x53'
|
||||
msg += b'\x00\x02\x00\x00\x27\x74\x53\x00\x3c\x00\x00\x27\xd8\x53\x00\x68\x00\x00\x28\x3c\x53\x05\x00\x00\x00\x28\xa0\x46\x43\x79\x00\x00\x00\x00\x29\x04\x46\x43\x48\x00\x00'
|
||||
msg += b'\x00\x00\x29\x68\x46\x42\x48\x33\x33\x00\x00\x29\xcc\x46\x42\x3e\x3d\x71\x00\x00\x2a\x30\x53\x00\x01\x00\x00\x2a\x94\x46\x43\x37\x00\x00\x00\x00\x2a\xf8\x46\x42\xce'
|
||||
msg += b'\x00\x00\x00\x00\x2b\x5c\x53\x00\x96\x00\x00\x2b\xc0\x53\x00\x10\x00\x00\x2c\x24\x46\x43\x90\x00\x00\x00\x00\x2c\x88\x46\x43\x95\x00\x00\x00\x00\x2c\xec\x53\x00\x06'
|
||||
msg += b'\x00\x00\x2d\x50\x53\x00\x06\x00\x00\x2d\xb4\x46\x43\x7d\x00\x00\x00\x00\x2e\x18\x46\x42\x3d\xeb\x85\x00\x00\x2e\x7c\x46\x42\x3d\xeb\x85\x00\x00\x2e\xe0\x53\x00\x03'
|
||||
msg += b'\x00\x00\x2f\x44\x53\x00\x03\x00\x00\x2f\xa8\x46\x42\x4d\xeb\x85\x00\x00\x30\x0c\x46\x42\x4d\xeb\x85\x00\x00\x30\x70\x53\x00\x03\x00\x00\x30\xd4\x53\x00\x03\x00\x00'
|
||||
msg += b'\x31\x38\x46\x42\x08\x00\x00\x00\x00\x31\x9c\x53\x00\x05\x00\x00\x32\x00\x53\x04\x00\x00\x00\x32\x64\x53\x00\x01\x00\x00\x32\xc8\x53\x13\x9c\x00\x00\x33\x2c\x53\x0f'
|
||||
msg += b'\xa0\x00\x00\x33\x90\x53\x00\x4f\x00\x00\x33\xf4\x53\x00\x66\x00\x00\x34\x58\x53\x03\xe8\x00\x00\x34\xbc\x53\x04\x00\x00\x00\x35\x20\x53\x00\x00\x00\x00\x35\x84\x53'
|
||||
msg += b'\x00\x00\x00\x00\x35\xe8\x53\x00\x00\x00\x00\x36\x4c\x53\x00\x00\x00\x01\x38\x80\x53\x00\x02\x00\x01\x38\x81\x53\x00\x01\x00\x01\x38\x82\x53\x00\x01\x00\x01\x38\x83'
|
||||
msg += b'\x53\x00\x00'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def InvDataSeq2_Zero(): # Data indication from the controller
|
||||
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\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00'
|
||||
msg += b'\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53'
|
||||
msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00\x00\x00\x01\xf5\x53\x00\x00\x00\x00'
|
||||
msg += b'\x01\xf6\x53\x00\x00\x00\x00\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00\x00\x00'
|
||||
msg += b'\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00'
|
||||
msg += b'\x00\x00\x02\x02\x53\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00\x00\x00\x02\x5a'
|
||||
msg += b'\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02'
|
||||
msg += b'\x60\x53\x00\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02\x65\x53\x00\x00\x00\x00'
|
||||
msg += b'\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00'
|
||||
msg += b'\x00\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3\x53\x00\x00\x00\x00\x02\xc4\x53'
|
||||
msg += b'\x00\x00\x00\x00\x02\xc5\x53\x00\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02\xca'
|
||||
msg += b'\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53\x00\x00\x00\x00\x03\x20\x53\x00\x00\x00\x00\x03\x84\x53\x50\x11\x00\x00\x03\xe8\x46\x43\x61\x66\x66\x00'
|
||||
msg += b'\x00\x04\x4c\x46\x3e\xeb\x85\x1f\x00\x00\x04\xb0\x46\x42\x48\x14\x7b\x00\x00\x05\x14\x53\x00\x17\x00\x00\x05\x78\x53\x00\x00\x00\x00\x05\xdc\x53\x02\x58\x00\x00\x06'
|
||||
msg += b'\x40\x46\x42\xd3\x66\x66\x00\x00\x06\xa4\x46\x42\x06\x66\x66\x00\x00\x07\x08\x46\x3f\xf4\x7a\xe1\x00\x00\x07\x6c\x46\x42\x81\x00\x00\x00\x00\x07\xd0\x46\x42\x06\x00'
|
||||
msg += b'\x00\x00\x00\x08\x34\x46\x3f\xae\x14\x7b\x00\x00\x08\x98\x46\x42\x36\xcc\xcd\x00\x00\x08\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x00\x00\x00\x00\x00\x00\x09\xc4'
|
||||
msg += b'\x46\x00\x00\x00\x00\x00\x00\x0a\x28\x46\x00\x00\x00\x00\x00\x00\x0a\x8c\x46\x00\x00\x00\x00\x00\x00\x0a\xf0\x46\x00\x00\x00\x00\x00\x00\x0b\x54\x46\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x0b\xb8\x46\x00\x00\x00\x00\x00\x00\x0c\x1c\x46\x00\x00\x00\x00\x00\x00\x0c\x80\x46\x00\x00\x00\x00\x00\x00\x0c\xe4\x46\x00\x00\x00\x00\x00\x00\x0d\x48\x46'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x0d\xac\x46\x00\x00\x00\x00\x00\x00\x0e\x10\x46\x00\x00\x00\x00\x00\x00\x0e\x74\x46\x00\x00\x00\x00\x00\x00\x0e\xd8\x46\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0\x53\x00\x00\x00\x00\x10\x04\x53\x55\xaa\x00\x00\x10\x68\x53\x00\x00\x00\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00'
|
||||
msg += b'\x00\x00\x11\x94\x53\x00\x00\x00\x00\x11\xf8\x53\xff\xff\x00\x00\x12\x5c\x53\xff\xff\x00\x00\x12\xc0\x53\x00\x02\x00\x00\x13\x24\x53\xff\xff\x00\x00\x13\x88\x53\xff'
|
||||
msg += b'\xff\x00\x00\x13\xec\x53\xff\xff\x00\x00\x14\x50\x53\xff\xff\x00\x00\x14\xb4\x53\xff\xff\x00\x00\x15\x18\x53\xff\xff\x00\x00\x15\x7c\x53\x00\x00\x00\x00\x27\x10\x53'
|
||||
msg += b'\x00\x02\x00\x00\x27\x74\x53\x00\x3c\x00\x00\x27\xd8\x53\x00\x68\x00\x00\x28\x3c\x53\x05\x00\x00\x00\x28\xa0\x46\x43\x79\x00\x00\x00\x00\x29\x04\x46\x43\x48\x00\x00'
|
||||
msg += b'\x00\x00\x29\x68\x46\x42\x48\x33\x33\x00\x00\x29\xcc\x46\x42\x3e\x3d\x71\x00\x00\x2a\x30\x53\x00\x01\x00\x00\x2a\x94\x46\x43\x37\x00\x00\x00\x00\x2a\xf8\x46\x42\xce'
|
||||
msg += b'\x00\x00\x00\x00\x2b\x5c\x53\x00\x96\x00\x00\x2b\xc0\x53\x00\x10\x00\x00\x2c\x24\x46\x43\x90\x00\x00\x00\x00\x2c\x88\x46\x43\x95\x00\x00\x00\x00\x2c\xec\x53\x00\x06'
|
||||
msg += b'\x00\x00\x2d\x50\x53\x00\x06\x00\x00\x2d\xb4\x46\x43\x7d\x00\x00\x00\x00\x2e\x18\x46\x42\x3d\xeb\x85\x00\x00\x2e\x7c\x46\x42\x3d\xeb\x85\x00\x00\x2e\xe0\x53\x00\x03'
|
||||
msg += b'\x00\x00\x2f\x44\x53\x00\x03\x00\x00\x2f\xa8\x46\x42\x4d\xeb\x85\x00\x00\x30\x0c\x46\x42\x4d\xeb\x85\x00\x00\x30\x70\x53\x00\x03\x00\x00\x30\xd4\x53\x00\x03\x00\x00'
|
||||
msg += b'\x31\x38\x46\x42\x08\x00\x00\x00\x00\x31\x9c\x53\x00\x05\x00\x00\x32\x00\x53\x04\x00\x00\x00\x32\x64\x53\x00\x01\x00\x00\x32\xc8\x53\x13\x9c\x00\x00\x33\x2c\x53\x0f'
|
||||
msg += b'\xa0\x00\x00\x33\x90\x53\x00\x4f\x00\x00\x33\xf4\x53\x00\x66\x00\x00\x34\x58\x53\x03\xe8\x00\x00\x34\xbc\x53\x04\x00\x00\x00\x35\x20\x53\x00\x00\x00\x00\x35\x84\x53'
|
||||
msg += b'\x00\x00\x00\x00\x35\xe8\x53\x00\x00\x00\x00\x36\x4c\x53\x00\x00\x00\x01\x38\x80\x53\x00\x02\x00\x01\x38\x81\x53\x00\x01\x00\x01\x38\x82\x53\x00\x01\x00\x01\x38\x83'
|
||||
msg += b'\x53\x00\x00'
|
||||
return msg
|
||||
|
||||
|
||||
def test_parse_control(ContrDataSeq):
|
||||
i = Infos()
|
||||
@@ -25,7 +95,7 @@ def test_parse_control(ContrDataSeq):
|
||||
pass
|
||||
|
||||
assert json.dumps(i.db) == json.dumps(
|
||||
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Signal_Strength": 100, "Power_On_Time": 29, "Data_Up_Interval": 300}})
|
||||
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com", "No_Inputs": 2}, "controller": {"Signal_Strength": 100, "Power_On_Time": 29, "Data_Up_Interval": 300}})
|
||||
|
||||
def test_parse_inverter(InvDataSeq):
|
||||
i = Infos()
|
||||
@@ -45,14 +115,14 @@ def test_parse_cont_and_invert(ContrDataSeq, InvDataSeq):
|
||||
|
||||
assert json.dumps(i.db) == json.dumps(
|
||||
{
|
||||
"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Signal_Strength": 100, "Power_On_Time": 29, "Data_Up_Interval": 300},
|
||||
"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com", "No_Inputs": 2}, "controller": {"Signal_Strength": 100, "Power_On_Time": 29, "Data_Up_Interval": 300},
|
||||
"inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T17E7307021D006A", "Equipment_Model": "TSOL-MS600"}})
|
||||
|
||||
|
||||
def test_build_ha_conf1(ContrDataSeq):
|
||||
i = Infos()
|
||||
tests = 0
|
||||
for d_json, comp, id in i.ha_confs(prfx="tsun/garagendach/", snr='123'):
|
||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", inv_snr='123', inv_node_id="garagendach/",proxy_node_id = 'proxy/', proxy_unique_id = '456'):
|
||||
|
||||
if id == 'out_power_123':
|
||||
assert comp == 'sensor'
|
||||
@@ -61,24 +131,23 @@ def test_build_ha_conf1(ContrDataSeq):
|
||||
|
||||
elif id == 'daily_gen_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Daily Generation", "stat_t": "tsun/garagendach/total", "dev_cla": "energy", "stat_cla": "total_increasing", "uniq_id": "daily_gen_123", "val_tpl": "{{value_json['Daily_Generation'] | float}}", "unit_of_meas": "kWh", "icon": "mdi:solar-power-variant", "dev": {"name": "Micro Inverter", "sa": "Micro Inverter", "via_device": "controller_123", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
assert d_json == json.dumps({"name": "Daily Generation", "stat_t": "tsun/garagendach/total", "dev_cla": "energy", "stat_cla": "total_increasing", "uniq_id": "daily_gen_123", "val_tpl": "{{value_json['Daily_Generation'] | float}}", "unit_of_meas": "kWh", "ic": "mdi:solar-power-variant", "dev": {"name": "Micro Inverter", "sa": "Micro Inverter", "via_device": "controller_123", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'power_pv1_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv1_123", "val_tpl": "{{ (value_json['pv1']['Power'] | float)}}", "unit_of_meas": "W", "icon": "mdi:gauge", "dev": {"name": "Module PV1", "sa": "Module PV1", "via_device": "inverter_123", "ids": ["input_pv1_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv1_123", "val_tpl": "{{ (value_json['pv1']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV1", "sa": "Module PV1", "via_device": "inverter_123", "ids": ["input_pv1_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'power_pv2_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv2_123", "val_tpl": "{{ (value_json['pv2']['Power'] | float)}}", "unit_of_meas": "W", "icon": "mdi:gauge", "dev": {"name": "Module PV2", "sa": "Module PV2", "via_device": "inverter_123", "ids": ["input_pv2_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
assert False # if we haven't received and parsed a control data msg, we don't know the number of inputs. In this case we only register the first one!!
|
||||
|
||||
|
||||
elif id == 'signal_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "icon": "mdi:wifi", "dev": {"name": "Controller", "sa": "Controller", "ids": ["controller_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller", "sa": "Controller", "via_device": "proxy", "ids": ["controller_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
assert tests==5
|
||||
assert tests==4
|
||||
|
||||
def test_build_ha_conf2(ContrDataSeq, InvDataSeq):
|
||||
i = Infos()
|
||||
@@ -89,30 +158,58 @@ def test_build_ha_conf2(ContrDataSeq, InvDataSeq):
|
||||
pass
|
||||
|
||||
tests = 0
|
||||
for d_json, comp, id in i.ha_confs(prfx="tsun/garagendach/", snr='123'):
|
||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", inv_snr='123', inv_node_id="garagendach/",proxy_node_id = 'proxy/', proxy_unique_id = '456'):
|
||||
|
||||
if id == 'out_power_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/grid", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "out_power_123", "val_tpl": "{{value_json['Output_Power'] | float}}", "unit_of_meas": "W", "dev": {"name": "Micro Inverter", "sa": "Micro Inverter", "via_device": "controller_123", "mdl": "TSOL-MS600", "mf": "TSUN", "sw": "V5.0.11", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'daily_gen_123':
|
||||
if id == 'daily_gen_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Daily Generation", "stat_t": "tsun/garagendach/total", "dev_cla": "energy", "stat_cla": "total_increasing", "uniq_id": "daily_gen_123", "val_tpl": "{{value_json['Daily_Generation'] | float}}", "unit_of_meas": "kWh", "icon": "mdi:solar-power-variant", "dev": {"name": "Micro Inverter", "sa": "Micro Inverter", "via_device": "controller_123", "mdl": "TSOL-MS600", "mf": "TSUN", "sw": "V5.0.11", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
assert d_json == json.dumps({"name": "Daily Generation", "stat_t": "tsun/garagendach/total", "dev_cla": "energy", "stat_cla": "total_increasing", "uniq_id": "daily_gen_123", "val_tpl": "{{value_json['Daily_Generation'] | float}}", "unit_of_meas": "kWh", "ic": "mdi:solar-power-variant", "dev": {"name": "Micro Inverter", "sa": "Micro Inverter", "via_device": "controller_123", "mdl": "TSOL-MS600", "mf": "TSUN", "sw": "V5.0.11", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'power_pv1_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv1_123", "val_tpl": "{{ (value_json['pv1']['Power'] | float)}}", "unit_of_meas": "W", "icon": "mdi:gauge", "dev": {"name": "Module PV1", "sa": "Module PV1", "via_device": "inverter_123", "ids": ["input_pv1_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv1_123", "val_tpl": "{{ (value_json['pv1']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV1", "sa": "Module PV1", "via_device": "inverter_123", "ids": ["input_pv1_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'power_pv2_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv2_123", "val_tpl": "{{ (value_json['pv2']['Power'] | float)}}", "unit_of_meas": "W", "icon": "mdi:gauge", "dev": {"name": "Module PV2", "sa": "Module PV2", "via_device": "inverter_123", "ids": ["input_pv2_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv2_123", "val_tpl": "{{ (value_json['pv2']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV2", "sa": "Module PV2", "via_device": "inverter_123", "ids": ["input_pv2_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
|
||||
elif id == 'signal_123':
|
||||
assert comp == 'sensor'
|
||||
assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "icon": "mdi:wifi", "dev": {"name": "Controller", "sa": "Controller", "mdl": "RSW-1-10001", "mf": "Raymon", "sw": "RSW_400_V1.00.06", "ids": ["controller_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
assert d_json == json.dumps({"name": "Signal Strength", "stat_t": "tsun/garagendach/controller", "dev_cla": None, "stat_cla": "measurement", "uniq_id": "signal_123", "val_tpl": "{{value_json[\'Signal_Strength\'] | int}}", "unit_of_meas": "%", "ic": "mdi:wifi", "dev": {"name": "Controller", "sa": "Controller", "via_device": "proxy", "mdl": "RSW-1-10001", "mf": "Raymon", "sw": "RSW_400_V1.00.06", "ids": ["controller_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||
tests +=1
|
||||
assert tests==5
|
||||
|
||||
def test_build_must_incr_total(InvDataSeq2, InvDataSeq2_Zero):
|
||||
i = Infos()
|
||||
tests = 0
|
||||
for key, update in i.parse (InvDataSeq2):
|
||||
if key == 'total':
|
||||
assert update == True
|
||||
tests +=1
|
||||
pass
|
||||
elif key == 'input':
|
||||
assert update == True
|
||||
tests +=1
|
||||
|
||||
assert tests==22
|
||||
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0, "Daily_Generation": 0.0, "Total_Generation": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0, "Daily_Generation": 0.0, "Total_Generation": 0.0}})
|
||||
|
||||
tests = 0
|
||||
for key, update in i.parse (InvDataSeq2_Zero):
|
||||
if key == 'total':
|
||||
tests +=1
|
||||
pass
|
||||
elif key == 'input':
|
||||
tests +=1
|
||||
|
||||
assert tests==22
|
||||
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 0.0, 'Total_Generation': 17.36})
|
||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 0.0, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.0, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0, "Daily_Generation": 0.0, "Total_Generation": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0, "Daily_Generation": 0.0, "Total_Generation": 0.0}})
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
import pytest
|
||||
from app.src.messages import Message, Control
|
||||
from app.src.config import Config
|
||||
from app.src.infos import Infos
|
||||
|
||||
# initialize the proxy statistics
|
||||
Infos.static_init()
|
||||
|
||||
class MemoryStream(Message):
|
||||
def __init__(self, msg, chunks = (0,)):
|
||||
|
||||
199
system_tests/test_tcp_socket.py
Normal file
199
system_tests/test_tcp_socket.py
Normal file
@@ -0,0 +1,199 @@
|
||||
# test_with_pytest.py and scapy
|
||||
#
|
||||
import pytest, socket, time
|
||||
#from scapy.all import *
|
||||
#from scapy.layers.inet import IP, TCP, TCP_client
|
||||
|
||||
def get_sn() -> bytes:
|
||||
return b'R170000000000001'
|
||||
|
||||
def get_inv_no() -> bytes:
|
||||
return b'T170000000000001'
|
||||
|
||||
def get_invalid_sn():
|
||||
return b'R170000000000002'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def MsgContactInfo(): # 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
|
||||
return b'\x00\x00\x00\x14\x10'+get_sn()+b'\x99\x00\x01'
|
||||
|
||||
@pytest.fixture
|
||||
def MsgContactInfo2(): # 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
|
||||
return b'\x00\x00\x00\x14\x10'+get_invalid_sn()+b'\x99\x00\x01'
|
||||
|
||||
@pytest.fixture
|
||||
def MsgTimeStampReq(): # Get Time Request message
|
||||
return b'\x00\x00\x00\x13\x10'+get_sn()+b'\x91\x22'
|
||||
|
||||
@pytest.fixture
|
||||
def MsgTimeStampResp(): # 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
|
||||
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'
|
||||
msg += b'\x6e\x00\x09\x2f\x90\x54\x0b\x52\x53\x57\x2d\x31\x2d\x31\x30\x30\x30\x31\x00\x09\x5a\x88\x54\x0f\x74\x2e\x72\x61\x79\x6d\x6f\x6e\x69\x6f\x74\x2e\x63\x6f\x6d\x00\x09\x5a\xec\x54'
|
||||
msg += b'\x1c\x6c\x6f\x67\x67\x65\x72\x2e\x74\x61\x6c\x65\x6e\x74\x2d\x6d\x6f\x6e\x69\x74\x6f\x72\x69\x6e\x67\x2e\x63\x6f\x6d\x00\x0d\x00\x20\x49\x00\x00\x00\x01\x00\x0c\x35\x00\x49\x00'
|
||||
msg += b'\x00\x00\x64\x00\x0c\x96\xa8\x49\x00\x00\x00\x1d\x00\x0c\x7f\x38\x49\x00\x00\x00\x01\x00\x0c\xfc\x38\x49\x00\x00\x00\x01\x00\x0c\xf8\x50\x49\x00\x00\x01\x2c\x00\x0c\x63\xe0\x49'
|
||||
msg += b'\x00\x00\x00\x00\x00\x0c\x67\xc8\x49\x00\x00\x00\x00\x00\x0c\x50\x58\x49\x00\x00\x00\x01\x00\x09\x5e\x70\x49\x00\x00\x13\x8d\x00\x09\x5e\xd4\x49\x00\x00\x13\x8d\x00\x09\x5b\x50'
|
||||
msg += b'\x49\x00\x00\x00\x02\x00\x0d\x04\x08\x49\x00\x00\x00\x00\x00\x07\xa1\x84\x49\x00\x00\x00\x01\x00\x0c\x50\x59\x49\x00\x00\x00\x4c\x00\x0d\x1f\x60\x49\x00\x00\x00\x00'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def MsgInvData(): # 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'
|
||||
msg += b'\x54\x10\x54\x31\x37\x45\x37\x33\x30\x37\x30\x32\x31\x44\x30\x30\x36\x41\x00\x00\x00\x32\x54\x0a\x54\x53\x4f\x4c\x2d\x4d\x53\x36\x30\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def MsgInverterInd(): # 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'
|
||||
msg += b'\x00\x00\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00'
|
||||
msg += b'\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53'
|
||||
msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00\x00\x00\x01\xf5\x53\x00\x00\x00\x00'
|
||||
msg += b'\x01\xf6\x53\x00\x00\x00\x00\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00\x00\x00'
|
||||
msg += b'\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00'
|
||||
msg += b'\x00\x00\x02\x02\x53\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00\x00\x00\x02\x5a'
|
||||
msg += b'\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02'
|
||||
msg += b'\x60\x53\x00\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02\x65\x53\x00\x00\x00\x00'
|
||||
msg += b'\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00'
|
||||
msg += b'\x00\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3\x53\x00\x00\x00\x00\x02\xc4\x53'
|
||||
msg += b'\x00\x00\x00\x00\x02\xc5\x53\x00\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02\xca'
|
||||
msg += b'\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53\x00\x00\x00\x00\x03\x20\x53\x00\x00\x00\x00\x03\x84\x53\x50\x11\x00\x00\x03\xe8\x46\x43\x61\x66\x66\x00'
|
||||
msg += b'\x00\x04\x4c\x46\x3e\xeb\x85\x1f\x00\x00\x04\xb0\x46\x42\x48\x14\x7b\x00\x00\x05\x14\x53\x00\x17\x00\x00\x05\x78\x53\x00\x00\x00\x00\x05\xdc\x53\x02\x58\x00\x00\x06'
|
||||
msg += b'\x40\x46\x42\xd3\x66\x66\x00\x00\x06\xa4\x46\x42\x06\x66\x66\x00\x00\x07\x08\x46\x3f\xf4\x7a\xe1\x00\x00\x07\x6c\x46\x42\x81\x00\x00\x00\x00\x07\xd0\x46\x42\x06\x00'
|
||||
msg += b'\x00\x00\x00\x08\x34\x46\x3f\xae\x14\x7b\x00\x00\x08\x98\x46\x42\x36\xcc\xcd\x00\x00\x08\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x00\x00\x00\x00\x00\x00\x09\xc4'
|
||||
msg += b'\x46\x00\x00\x00\x00\x00\x00\x0a\x28\x46\x00\x00\x00\x00\x00\x00\x0a\x8c\x46\x00\x00\x00\x00\x00\x00\x0a\xf0\x46\x00\x00\x00\x00\x00\x00\x0b\x54\x46\x3f\xd9\x99\x9a'
|
||||
msg += b'\x00\x00\x0b\xb8\x46\x41\x8a\xe1\x48\x00\x00\x0c\x1c\x46\x3f\x8a\x3d\x71\x00\x00\x0c\x80\x46\x41\x1b\xd7\x0a\x00\x00\x0c\xe4\x46\x3f\x1e\xb8\x52\x00\x00\x0d\x48\x46'
|
||||
msg += b'\x40\xf3\xd7\x0a\x00\x00\x0d\xac\x46\x00\x00\x00\x00\x00\x00\x0e\x10\x46\x00\x00\x00\x00\x00\x00\x0e\x74\x46\x00\x00\x00\x00\x00\x00\x0e\xd8\x46\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0\x53\x00\x00\x00\x00\x10\x04\x53\x55\xaa\x00\x00\x10\x68\x53\x00\x00\x00\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00'
|
||||
msg += b'\x00\x00\x11\x94\x53\x00\x00\x00\x00\x11\xf8\x53\xff\xff\x00\x00\x12\x5c\x53\xff\xff\x00\x00\x12\xc0\x53\x00\x02\x00\x00\x13\x24\x53\xff\xff\x00\x00\x13\x88\x53\xff'
|
||||
msg += b'\xff\x00\x00\x13\xec\x53\xff\xff\x00\x00\x14\x50\x53\xff\xff\x00\x00\x14\xb4\x53\xff\xff\x00\x00\x15\x18\x53\xff\xff\x00\x00\x15\x7c\x53\x00\x00\x00\x00\x27\x10\x53'
|
||||
msg += b'\x00\x02\x00\x00\x27\x74\x53\x00\x3c\x00\x00\x27\xd8\x53\x00\x68\x00\x00\x28\x3c\x53\x05\x00\x00\x00\x28\xa0\x46\x43\x79\x00\x00\x00\x00\x29\x04\x46\x43\x48\x00\x00'
|
||||
msg += b'\x00\x00\x29\x68\x46\x42\x48\x33\x33\x00\x00\x29\xcc\x46\x42\x3e\x3d\x71\x00\x00\x2a\x30\x53\x00\x01\x00\x00\x2a\x94\x46\x43\x37\x00\x00\x00\x00\x2a\xf8\x46\x42\xce'
|
||||
msg += b'\x00\x00\x00\x00\x2b\x5c\x53\x00\x96\x00\x00\x2b\xc0\x53\x00\x10\x00\x00\x2c\x24\x46\x43\x90\x00\x00\x00\x00\x2c\x88\x46\x43\x95\x00\x00\x00\x00\x2c\xec\x53\x00\x06'
|
||||
msg += b'\x00\x00\x2d\x50\x53\x00\x06\x00\x00\x2d\xb4\x46\x43\x7d\x00\x00\x00\x00\x2e\x18\x46\x42\x3d\xeb\x85\x00\x00\x2e\x7c\x46\x42\x3d\xeb\x85\x00\x00\x2e\xe0\x53\x00\x03'
|
||||
msg += b'\x00\x00\x2f\x44\x53\x00\x03\x00\x00\x2f\xa8\x46\x42\x4d\xeb\x85\x00\x00\x30\x0c\x46\x42\x4d\xeb\x85\x00\x00\x30\x70\x53\x00\x03\x00\x00\x30\xd4\x53\x00\x03\x00\x00'
|
||||
msg += b'\x31\x38\x46\x42\x08\x00\x00\x00\x00\x31\x9c\x53\x00\x05\x00\x00\x32\x00\x53\x04\x00\x00\x00\x32\x64\x53\x00\x01\x00\x00\x32\xc8\x53\x13\x9c\x00\x00\x33\x2c\x53\x0f'
|
||||
msg += b'\xa0\x00\x00\x33\x90\x53\x00\x4f\x00\x00\x33\xf4\x53\x00\x66\x00\x00\x34\x58\x53\x03\xe8\x00\x00\x34\xbc\x53\x04\x00\x00\x00\x35\x20\x53\x00\x00\x00\x00\x35\x84\x53'
|
||||
msg += b'\x00\x00\x00\x00\x35\xe8\x53\x00\x00\x00\x00\x36\x4c\x53\x00\x00\x00\x01\x38\x80\x53\x00\x02\x00\x01\x38\x81\x53\x00\x01\x00\x01\x38\x82\x53\x00\x01\x00\x01\x38\x83'
|
||||
msg += b'\x53\x00\x00'
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def ClientConnection():
|
||||
#host = '172.16.30.7'
|
||||
host = 'logger.talent-monitoring.com'
|
||||
#host = '127.0.0.1'
|
||||
port = 5005
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.connect((host, port))
|
||||
s.settimeout(1)
|
||||
yield s
|
||||
s.close()
|
||||
|
||||
def tempClientConnection():
|
||||
#host = '172.16.30.7'
|
||||
host = 'logger.talent-monitoring.com'
|
||||
#host = '127.0.0.1'
|
||||
port = 5005
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.connect((host, port))
|
||||
s.settimeout(1)
|
||||
yield s
|
||||
s.close()
|
||||
|
||||
def test_open_close():
|
||||
try:
|
||||
for s in tempClientConnection():
|
||||
pass
|
||||
except:
|
||||
assert False
|
||||
assert True
|
||||
|
||||
def test_send_contact_info1(ClientConnection, MsgContactInfo, MsgContactResp):
|
||||
s = ClientConnection
|
||||
try:
|
||||
s.sendall(MsgContactInfo)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
assert data == MsgContactResp
|
||||
|
||||
def test_send_contact_info2(ClientConnection, MsgContactInfo2, MsgContactInfo, MsgContactResp):
|
||||
s = ClientConnection
|
||||
try:
|
||||
s.sendall(MsgContactInfo2)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
assert True
|
||||
else:
|
||||
assert False
|
||||
|
||||
try:
|
||||
s.sendall(MsgContactInfo)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
assert data == MsgContactResp
|
||||
|
||||
|
||||
|
||||
def test_send_contact_resp(ClientConnection, MsgContactResp):
|
||||
s = ClientConnection
|
||||
try:
|
||||
s.sendall(MsgContactResp)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
assert True
|
||||
else:
|
||||
assert data ==''
|
||||
|
||||
def test_send_ctrl_data(ClientConnection, MsgTimeStampReq, MsgTimeStampResp, MsgContollerInd):
|
||||
s = ClientConnection
|
||||
try:
|
||||
s.sendall(MsgTimeStampReq)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
# time.sleep(2.5)
|
||||
# assert data == MsgTimeStampResp
|
||||
try:
|
||||
s.sendall(MsgContollerInd)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
def test_send_inv_data(ClientConnection, MsgTimeStampReq, MsgTimeStampResp, MsgInvData, MsgInverterInd):
|
||||
s = ClientConnection
|
||||
try:
|
||||
s.sendall(MsgTimeStampReq)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
# time.sleep(32.5)
|
||||
# assert data == MsgTimeStampResp
|
||||
try:
|
||||
s.sendall(MsgInvData)
|
||||
data = s.recv(1024)
|
||||
s.sendall(MsgInverterInd)
|
||||
data = s.recv(1024)
|
||||
except TimeoutError:
|
||||
pass
|
||||
Reference in New Issue
Block a user