Compare commits
74 Commits
v0.12.0
...
titan-scan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f4d3740d6 | ||
|
|
abdc323dc6 | ||
|
|
35efa65410 | ||
|
|
399d0b58ff | ||
|
|
2b4bd2922c | ||
|
|
3eba999a2b | ||
|
|
ad8a902ac6 | ||
|
|
fda4008036 | ||
|
|
e7e693c0b6 | ||
|
|
98665b3aac | ||
|
|
6b488e5269 | ||
|
|
010ae2b2c7 | ||
|
|
24840e67c0 | ||
|
|
2dd89d3174 | ||
|
|
c84b610229 | ||
|
|
5dc890dde0 | ||
|
|
0c10cc32dc | ||
|
|
71f581eb0b | ||
|
|
a4acddd769 | ||
|
|
724f6f3b22 | ||
|
|
18c3020282 | ||
|
|
e127716317 | ||
|
|
5790b657d0 | ||
|
|
5d4ff9051d | ||
|
|
595b68ba03 | ||
|
|
d7628689f0 | ||
|
|
e074a39f5a | ||
|
|
9852f44dfa | ||
|
|
c7d0a91371 | ||
|
|
5b68610f78 | ||
|
|
0b79a37fe7 | ||
|
|
00e9a4534d | ||
|
|
3a94afb48d | ||
|
|
949f3c9608 | ||
|
|
cd2f41a713 | ||
|
|
84034127e3 | ||
|
|
22d59ed659 | ||
|
|
cfe2c9cb9d | ||
|
|
39aba31bbd | ||
|
|
a1441fb4fd | ||
|
|
f2ade43410 | ||
|
|
8f695518bd | ||
|
|
b3068a256c | ||
|
|
2336955bb8 | ||
|
|
8a745b2d10 | ||
|
|
518375e8a8 | ||
|
|
b57ded1a73 | ||
|
|
41aeac4168 | ||
|
|
5a0ef30ceb | ||
|
|
4c15495670 | ||
|
|
ca610b15ff | ||
|
|
0c824b4a2a | ||
|
|
95f817a92b | ||
|
|
2ef7a7e201 | ||
|
|
2be0ef67af | ||
|
|
43700b3da8 | ||
|
|
7b6810cb46 | ||
|
|
aa7c1832ef | ||
|
|
73526b7dc6 | ||
|
|
b6761e7517 | ||
|
|
209956865b | ||
|
|
bc54944077 | ||
|
|
37c977c2fe | ||
|
|
d5c369e5fe | ||
|
|
89c2c11ed9 | ||
|
|
eea725b8da | ||
|
|
0b437cf3bc | ||
|
|
ae4bcee41f | ||
|
|
edc0f7a6af | ||
|
|
9ffcfbc9ce | ||
|
|
b7c63b5cf8 | ||
|
|
af81aef07c | ||
|
|
f216c2434e | ||
|
|
39540678fb |
@@ -1,3 +0,0 @@
|
||||
[run]
|
||||
branch = True
|
||||
relative_files = True
|
||||
@@ -1,2 +1,3 @@
|
||||
[run]
|
||||
branch = True
|
||||
relative_files = True
|
||||
@@ -1,9 +0,0 @@
|
||||
# example file for the .env file. The .env set private values
|
||||
# which are needed for builing containers
|
||||
|
||||
# registry for debug an dev container
|
||||
PRIVAT_CONTAINER_REGISTRY=docker.io/<user>/
|
||||
|
||||
# registry for official container (preview, rc, rel)
|
||||
PUBLIC_CONTAINER_REGISTRY=ghcr.io/<user>/
|
||||
PUBLIC_CR_KEY=
|
||||
9
.github/workflows/python-app.yml
vendored
9
.github/workflows/python-app.yml
vendored
@@ -26,12 +26,11 @@ permissions:
|
||||
|
||||
env:
|
||||
TZ: "Europe/Berlin"
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -54,13 +53,13 @@ jobs:
|
||||
flake8 --exit-zero --ignore=C901,E121,E123,E126,E133,E226,E241,E242,E704,W503,W504,W505 --format=pylint --output-file=output_flake.txt --exclude=*.pyc app/src/
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
python -m pytest app --cov=app/src --cov-config=.cover_ghaction_rc --cov-report=xml
|
||||
python -m pytest app --cov=app/src --cov-report=xml
|
||||
coverage report
|
||||
- name: Analyze with SonarCloud
|
||||
if: ${{ env.SONAR_TOKEN != 0 }}
|
||||
uses: SonarSource/sonarqube-scan-action@v4
|
||||
uses: SonarSource/sonarcloud-github-action@v3.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
projectBaseDir: .
|
||||
args:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,15 +1,11 @@
|
||||
__pycache__
|
||||
.pytest_cache
|
||||
.venv/**
|
||||
bin/**
|
||||
mosquitto/**
|
||||
homeassistant/**
|
||||
ha_addons/ha_addon/rootfs/home/proxy/*
|
||||
ha_addons/ha_addon/rootfs/requirements.txt
|
||||
tsun_proxy/**
|
||||
Doku/**
|
||||
.DS_Store
|
||||
.coverage
|
||||
.env
|
||||
.venv
|
||||
coverage.xml
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
ignored:
|
||||
- SC1091
|
||||
@@ -1 +0,0 @@
|
||||
3.13.1
|
||||
20
.vscode/settings.json
vendored
20
.vscode/settings.json
vendored
@@ -1,20 +1,16 @@
|
||||
{
|
||||
"python.analysis.extraPaths": [
|
||||
"app/src",
|
||||
"app/tests",
|
||||
".venv/lib",
|
||||
],
|
||||
"python.testing.pytestArgs": [
|
||||
"-vvv",
|
||||
"-vv",
|
||||
"app",
|
||||
"--cov=app/src",
|
||||
"--cov-report=xml",
|
||||
"app",
|
||||
"--cov-report=html",
|
||||
"system_tests"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"flake8.args": [
|
||||
"--extend-exclude=app/tests/*.py,system_tests/*.py"
|
||||
"--extend-exclude=app/tests/*.py system_tests/*.py"
|
||||
],
|
||||
"sonarlint.connectedMode.project": {
|
||||
"connectionId": "s-allius",
|
||||
@@ -22,11 +18,5 @@
|
||||
},
|
||||
"files.exclude": {
|
||||
"**/*.pyi": true
|
||||
},
|
||||
"python.analysis.typeEvaluation.deprecateTypingAliases": true,
|
||||
"python.autoComplete.extraPaths": [
|
||||
".venv/lib"
|
||||
],
|
||||
"coverage-gutters.coverageBaseDir": "tsun",
|
||||
"makefile.configureOnOpen": false
|
||||
}
|
||||
}
|
||||
31
CHANGELOG.md
31
CHANGELOG.md
@@ -7,40 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [unreleased]
|
||||
|
||||
## [0.12.0] - 2024-12-22
|
||||
|
||||
- add hadolint configuration
|
||||
- detect usage of a local DNS resolver [#37](https://github.com/s-allius/tsun-gen3-proxy/issues/37)
|
||||
- path for logs is now configurable by cli args
|
||||
- configure the number of keeped logfiles by cli args
|
||||
- add DOCS.md and CHANGELOG.md for add-ons
|
||||
- pin library version und update them with renovate
|
||||
- build config.yaml for add-ons by a jinja2 template
|
||||
- use gnu make to build proxy and add-on
|
||||
- make the configuration more flexible, add command line args to control this
|
||||
- fix the python path so we don't need special import paths for unit tests anymore
|
||||
- add emulator mode [#205](https://github.com/s-allius/tsun-gen3-proxy/issues/205)
|
||||
- ignore inverter replays which a older than 1 day [#246](https://github.com/s-allius/tsun-gen3-proxy/issues/246)
|
||||
- support test coverage in vscode
|
||||
- upgrade SonarQube action to version 4
|
||||
- update github action to Ubuntu 24-04
|
||||
- add initial support for home assistant add-ons from @mime24
|
||||
- github action: use ubuntu 24.04 and sonar-scanner-action 4 [#222](https://github.com/s-allius/tsun-gen3-proxy/issues/222)
|
||||
- migrate paho.mqtt CallbackAPIVersion to VERSION2 [#224](https://github.com/s-allius/tsun-gen3-proxy/issues/224)
|
||||
- add PROD_COMPL_TYPE to trace
|
||||
- add SolarmanV5 messages builder
|
||||
- report inverter alarms and faults per MQTT [#7](https://github.com/s-allius/tsun-gen3-proxy/issues/7)
|
||||
|
||||
## [0.11.1] - 2024-11-20
|
||||
|
||||
- fix pytest setup that can be startet from the rootdir
|
||||
- support python venv environment
|
||||
- add pytest.ini
|
||||
- move common settings from .vscode/settings.json into pytest.ini
|
||||
- add missing requirements
|
||||
- fix import paths for pytests
|
||||
- Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.5 to 3.10.11.
|
||||
|
||||
## [0.11.0] - 2024-10-13
|
||||
|
||||
- fix healthcheck on infrastructure with IPv6 support [#196](https://github.com/s-allius/tsun-gen3-proxy/issues/196)
|
||||
|
||||
14
Makefile
14
Makefile
@@ -1,14 +0,0 @@
|
||||
.PHONY: build clean addon-dev addon-debug addon-rc addon-rel debug dev preview rc rel
|
||||
|
||||
debug dev preview rc rel:
|
||||
$(MAKE) -C app $@
|
||||
|
||||
clean build:
|
||||
$(MAKE) -C ha_addons $@
|
||||
|
||||
addon-dev addon-debug addon-rc addon-rel:
|
||||
$(MAKE) -C ha_addons $(patsubst addon-%,%,$@)
|
||||
|
||||
check-docker-compose:
|
||||
docker-compose config -q
|
||||
|
||||
42
README.md
42
README.md
@@ -9,13 +9,13 @@
|
||||
<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-2.3.0-lightblue.svg"></a>
|
||||
<a href="https://libraries.io/pypi/aiocron"><img alt="Supported aiocron versions" src="https://img.shields.io/badge/aiocron-1.8-lightblue.svg"></a>
|
||||
<a href="https://toml.io/en/v1.0.0"><img alt="Supported toml versions" src="https://img.shields.io/badge/toml-1.0.0-lightblue.svg"></a>
|
||||
<a href="https://toml.io/en/v1.0.0"><img alt="Supported toml versions" src="https://img.shields.io/badge/toml-1.0.0-lightblue.svg"></a>
|
||||
<br>
|
||||
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=alert_status"><img alt="The quality gate status" src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=alert_status"></a>
|
||||
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=bugs"><img alt="No of bugs" src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=bugs"></a>
|
||||
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=code_smells"><img alt="No of code-smells" src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=code_smells"></a>
|
||||
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=alert_status"><img src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=alert_status"></a>
|
||||
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=bugs"><img src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=bugs"></a>
|
||||
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=code_smells"><img src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=code_smells"></a>
|
||||
<br>
|
||||
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=coverage"><img alt="Test coverage in percent" src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=coverage"></a>
|
||||
<a href="https://sonarcloud.io/component_measures?id=s-allius_tsun-gen3-proxy&metric=coverage"><img src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=coverage"></a>
|
||||
</p>
|
||||
|
||||
# Overview
|
||||
@@ -28,9 +28,6 @@ Through this, the inverter then establishes a connection to the proxy and the pr
|
||||
|
||||
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.
|
||||
|
||||
Alternatively you can run the TSUN-Proxy as a Home Assistant Add-on. The installation of this add-on is pretty straightforward and not different in comparison to installing any other custom Home Assistant add-on.
|
||||
Follow the Instructions mentioned in the add-on subdirectory `ha_addons`.
|
||||
|
||||
<br>
|
||||
ℹ️ 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.
|
||||
<br><br>
|
||||
@@ -68,20 +65,11 @@ Here are some screenshots of how the inverter is displayed in the Home Assistant
|
||||
|
||||
## Requirements
|
||||
|
||||
### for Docker Installation
|
||||
|
||||
- A running Docker engine to host the container
|
||||
- Ability to loop the proxy into the connection between the inverter and the TSUN cloud
|
||||
|
||||
### for Home Assistant Add-on Installation
|
||||
|
||||
- Running Home Assistant on Home Assistant OS or Supervised. Container and Core installations doesn't support add-ons.
|
||||
- Ability to loop the proxy into the connection between the inverter and the TSUN cloud
|
||||
|
||||
# Getting Started
|
||||
|
||||
## for Docker Installation
|
||||
|
||||
To run the proxy, you first need to create the image. You can do this quite simply as follows:
|
||||
|
||||
```sh
|
||||
@@ -107,22 +95,8 @@ With this information we can customize the `docker run`` statement:
|
||||
docker run --dns '8.8.8.8' --env 'UID=1050' -p '5005:5005' -p '10000:10000' -v ./config:/home/tsun-proxy/config -v ./log:/home/tsun-proxy/log tsun-proxy
|
||||
```
|
||||
|
||||
## for Home Assistant Add-on Installation
|
||||
|
||||
1. Add the repository URL to the Home Assistant add-on store
|
||||
[![Add repository on my Home Assistant][repository-badge]][repository-url]
|
||||
2. Reload the add-on store page
|
||||
3. Click the "Install" button to install the add-on.
|
||||
|
||||
# Configuration
|
||||
|
||||
```txt
|
||||
❗The following description applies to the Docker installation. When installing the Home Assistant add-on, the
|
||||
configuration is carried out via the Home Assistant UI. Some of the options described below are not required for
|
||||
this. Additionally, creating a config.toml file is not necessary. However, for a general understanding of the
|
||||
configuration and functionality, it is helpful to read the following description.
|
||||
```
|
||||
|
||||
The configuration consists of several parts. First, the container and the proxy itself must be configured, and then the connection of the inverter to the proxy must be set up, which is done differently depending on the inverter generation
|
||||
|
||||
For GEN3PLUS inverters, this can be done easily via the web interface of the inverter. The GEN3 inverters do not have a web interface, so the proxy is integrated via a modified DNS resolution.
|
||||
@@ -301,7 +275,7 @@ modbus_polling = true # Enable optional MODBUS polling
|
||||
|
||||
# if your inverter supports SSL connections you must use the client_mode. Pls, uncomment
|
||||
# the next line and configure the fixed IP of your inverter
|
||||
#client_mode = {host = '192.168.0.1', port = 8899, forward = true}
|
||||
#client_mode = {host = '192.168.0.1', port = 8899}
|
||||
|
||||
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
@@ -346,6 +320,7 @@ In this case, you MUST NOT change the port or the host address, as this may caus
|
||||
require a complete reset. Use the configuration in client mode instead.
|
||||
```
|
||||
|
||||
|
||||
If access to the web interface does not work, it can also be redirected via DNS redirection, as is necessary for the GEN3 inverters.
|
||||
|
||||
## Client Mode (GEN3PLUS only)
|
||||
@@ -433,6 +408,3 @@ We're very happy to receive contributions to this project! You can get started b
|
||||
## Changelog
|
||||
|
||||
The changelog lives in [CHANGELOG.md](https://github.com/s-allius/tsun-gen3-proxy/blob/main/CHANGELOG.md). It follows the principles of [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
[repository-badge]: https://img.shields.io/badge/Add%20repository%20to%20my-Home%20Assistant-41BDF5?logo=home-assistant&style=for-the-badge
|
||||
[repository-url]: https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Fs-allius%2Fha-addons
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
0.12.0
|
||||
@@ -4,13 +4,14 @@ ARG GID=1000
|
||||
|
||||
#
|
||||
# first stage for our base image
|
||||
FROM python:3.13-alpine AS base
|
||||
FROM python:3.12-alpine AS base
|
||||
USER root
|
||||
|
||||
COPY --chmod=0700 ./hardening_base.sh /
|
||||
COPY --chmod=0700 ./hardening_base.sh .
|
||||
RUN apk upgrade --no-cache && \
|
||||
apk add --no-cache su-exec=0.2-r3 && \
|
||||
/hardening_base.sh && \
|
||||
rm /hardening_base.sh
|
||||
apk add --no-cache su-exec && \
|
||||
./hardening_base.sh && \
|
||||
rm ./hardening_base.sh
|
||||
|
||||
#
|
||||
# second stage for building wheels packages
|
||||
@@ -18,8 +19,8 @@ FROM base AS builder
|
||||
|
||||
# copy the dependencies file to the root dir and install requirements
|
||||
COPY ./requirements.txt /root/
|
||||
RUN apk add --no-cache build-base=0.5-r3 && \
|
||||
python -m pip install --no-cache-dir pip==24.3.1 wheel==0.45.1 && \
|
||||
RUN apk add --no-cache build-base && \
|
||||
python -m pip install --no-cache-dir -U pip wheel && \
|
||||
python -OO -m pip wheel --no-cache-dir --wheel-dir=/root/wheels -r /root/requirements.txt
|
||||
|
||||
|
||||
@@ -49,9 +50,9 @@ VOLUME ["/home/$SERVICE_NAME/log", "/home/$SERVICE_NAME/config"]
|
||||
# and unistall python packages and alpine package manger to reduce attack surface
|
||||
COPY --from=builder /root/wheels /root/wheels
|
||||
COPY --chmod=0700 ./hardening_final.sh .
|
||||
RUN python -m pip install --no-cache-dir --no-cache --no-index /root/wheels/* && \
|
||||
RUN python -m pip install --no-cache --no-index /root/wheels/* && \
|
||||
rm -rf /root/wheels && \
|
||||
python -m pip uninstall --yes wheel pip && \
|
||||
python -m pip uninstall --yes setuptools wheel pip && \
|
||||
apk --purge del apk-tools && \
|
||||
./hardening_final.sh && \
|
||||
rm ./hardening_final.sh
|
||||
|
||||
67
app/Makefile
67
app/Makefile
@@ -1,67 +0,0 @@
|
||||
#!make
|
||||
include ../.env
|
||||
|
||||
SHELL = /bin/sh
|
||||
IMAGE = tsun-gen3-proxy
|
||||
|
||||
|
||||
# Folders
|
||||
SRC=.
|
||||
SRC_PROXY=$(SRC)/src
|
||||
CNF_PROXY=$(SRC)/config
|
||||
|
||||
DST=rootfs
|
||||
DST_PROXY=$(DST)/home/proxy
|
||||
|
||||
# collect source files
|
||||
SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/*.ini)\
|
||||
$(wildcard $(SRC_PROXY)/cnf/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/gen3/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/gen3plus/*.py)
|
||||
CNF_FILES := $(wildcard $(CNF_PROXY)/*.toml)
|
||||
|
||||
# determine destination files
|
||||
TARGET_FILES = $(SRC_FILES:$(SRC_PROXY)/%=$(DST_PROXY)/%)
|
||||
CONFIG_FILES = $(CNF_FILES:$(CNF_PROXY)/%=$(DST_PROXY)/%)
|
||||
|
||||
export BUILD_DATE := ${shell date -Iminutes}
|
||||
VERSION := $(shell cat $(SRC)/.version)
|
||||
export MAJOR := $(shell echo $(VERSION) | cut -f1 -d.)
|
||||
|
||||
PUBLIC_URL := $(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f1 -d/)
|
||||
PUBLIC_USER :=$(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f2 -d/)
|
||||
|
||||
|
||||
dev debug:
|
||||
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE)
|
||||
export VERSION=$(VERSION)-$@ && \
|
||||
export IMAGE=$(PRIVAT_CONTAINER_REGISTRY)$(IMAGE) && \
|
||||
docker buildx bake -f docker-bake.hcl $@
|
||||
|
||||
preview rc rel:
|
||||
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PUBLIC_CONTAINER_REGISTRY)$(IMAGE)
|
||||
@echo login at $(PUBLIC_URL) as $(PUBLIC_USER)
|
||||
@DO_LOGIN="$(shell echo $(PUBLIC_CR_KEY) | docker login $(PUBLIC_URL) -u $(PUBLIC_USER) --password-stdin)"
|
||||
export VERSION=$(VERSION)-$@ && \
|
||||
export IMAGE=$(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) && \
|
||||
docker buildx bake -f docker-bake.hcl $@
|
||||
|
||||
|
||||
|
||||
.PHONY: debug dev preview rc rel
|
||||
|
||||
|
||||
$(CONFIG_FILES): $(DST_PROXY)/% : $(CNF_PROXY)/%
|
||||
@echo Copy $< to $@
|
||||
@mkdir -p $(@D)
|
||||
@cp $< $@
|
||||
|
||||
$(TARGET_FILES): $(DST_PROXY)/% : $(SRC_PROXY)/%
|
||||
@echo Copy $< to $@
|
||||
@mkdir -p $(@D)
|
||||
@cp $< $@
|
||||
|
||||
$(DST)/requirements.txt : $(SRC)/requirements.txt
|
||||
@echo Copy $< to $@
|
||||
@cp $< $@
|
||||
51
app/build.sh
Executable file
51
app/build.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
# Usage: ./build.sh [dev|rc|rel]
|
||||
# dev: development build
|
||||
# rc: release candidate build
|
||||
# rel: release build and push to ghcr.io
|
||||
# Note: for release build, you need to set GHCR_TOKEN
|
||||
# export GHCR_TOKEN=<YOUR_GITHUB_TOKEN> in your .zprofile
|
||||
# see also: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
|
||||
|
||||
|
||||
set -e
|
||||
|
||||
BUILD_DATE=$(date -Iminutes)
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
VERSION=$(git describe --tags --abbrev=0)
|
||||
VERSION="${VERSION:1}"
|
||||
arr=(${VERSION//./ })
|
||||
MAJOR=${arr[0]}
|
||||
IMAGE=tsun-gen3-proxy
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
if [[ $1 == debug ]] || [[ $1 == dev ]] ;then
|
||||
IMAGE=docker.io/sallius/${IMAGE}
|
||||
VERSION=${VERSION}+$1
|
||||
elif [[ $1 == rc ]] || [[ $1 == rel ]] || [[ $1 == preview ]] ;then
|
||||
IMAGE=ghcr.io/s-allius/${IMAGE}
|
||||
echo 'login to ghcr.io'
|
||||
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
|
||||
else
|
||||
echo argument missing!
|
||||
echo try: $0 '[debug|dev|preview|rc|rel]'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export IMAGE
|
||||
export VERSION
|
||||
export BUILD_DATE
|
||||
export BRANCH
|
||||
export MAJOR
|
||||
|
||||
echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE
|
||||
docker buildx bake -f app/docker-bake.hcl $1
|
||||
|
||||
echo -e "${BLUE} => checking docker-compose.yaml file${NC}"
|
||||
docker-compose config -q
|
||||
echo
|
||||
echo -e "${GREEN}${BUILD_DATE} => Version: ${VERSION}${NC} finished"
|
||||
echo
|
||||
@@ -149,7 +149,7 @@ modbus_polling = true # Enable optional MODBUS polling
|
||||
|
||||
# if your inverter supports SSL connections you must use the client_mode. Pls, uncomment
|
||||
# the next line and configure the fixed IP of your inverter
|
||||
#client_mode = {host = '192.168.0.1', port = 8899, forward = true}
|
||||
#client_mode = {host = '192.168.0.1', port = 8899}
|
||||
|
||||
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
|
||||
@@ -18,7 +18,7 @@ variable "DESCRIPTION" {
|
||||
}
|
||||
|
||||
target "_common" {
|
||||
context = "."
|
||||
context = "app"
|
||||
dockerfile = "Dockerfile"
|
||||
args = {
|
||||
VERSION = "${VERSION}"
|
||||
|
||||
@@ -2,7 +2,5 @@
|
||||
pytest
|
||||
pytest-asyncio
|
||||
pytest-cov
|
||||
python-dotenv
|
||||
mock
|
||||
coverage
|
||||
jinja2-cli
|
||||
coverage
|
||||
@@ -1,4 +1,4 @@
|
||||
aiomqtt==2.3.0
|
||||
schema==0.7.7
|
||||
aiocron==1.8
|
||||
aiohttp==3.11.10
|
||||
aiohttp==3.10.5
|
||||
@@ -6,10 +6,16 @@ from asyncio import StreamReader, StreamWriter
|
||||
from typing import Self
|
||||
from itertools import count
|
||||
|
||||
from proxy import Proxy
|
||||
from byte_fifo import ByteFifo
|
||||
from async_ifc import AsyncIfc
|
||||
from infos import Infos
|
||||
if __name__ == "app.src.async_stream":
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.byte_fifo import ByteFifo
|
||||
from app.src.async_ifc import AsyncIfc
|
||||
from app.src.infos import Infos
|
||||
else: # pragma: no cover
|
||||
from proxy import Proxy
|
||||
from byte_fifo import ByteFifo
|
||||
from async_ifc import AsyncIfc
|
||||
from infos import Infos
|
||||
|
||||
|
||||
import gc
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
from messages import hex_dump_str, hex_dump_memory
|
||||
|
||||
if __name__ == "app.src.byte_fifo":
|
||||
from app.src.messages import hex_dump_str, hex_dump_memory
|
||||
else: # pragma: no cover
|
||||
from messages import hex_dump_str, hex_dump_memory
|
||||
|
||||
|
||||
class ByteFifo:
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
'''Config Reader module which handles config values from the environment'''
|
||||
|
||||
import os
|
||||
from cnf.config import ConfigIfc
|
||||
|
||||
|
||||
class ConfigReadEnv(ConfigIfc):
|
||||
'''Reader for environment values of the configuration'''
|
||||
|
||||
def get_config(self) -> dict:
|
||||
conf = {}
|
||||
data = [
|
||||
('mqtt.host', 'MQTT_HOST'),
|
||||
('mqtt.port', 'MQTT_PORT'),
|
||||
('mqtt.user', 'MQTT_USER'),
|
||||
('mqtt.passwd', 'MQTT_PASSWORD'),
|
||||
]
|
||||
for key, env_var in data:
|
||||
val = os.getenv(env_var)
|
||||
if val:
|
||||
self._extend_key(conf, key, val)
|
||||
return conf
|
||||
|
||||
def descr(self):
|
||||
return "environment"
|
||||
@@ -1,46 +0,0 @@
|
||||
'''Config Reader module which handles *.json config files'''
|
||||
|
||||
import json
|
||||
from cnf.config import ConfigIfc
|
||||
|
||||
|
||||
class ConfigReadJson(ConfigIfc):
|
||||
'''Reader for json config files'''
|
||||
def __init__(self, cnf_file='/data/options.json'):
|
||||
'''Read a json file and add the settings to the config'''
|
||||
if not isinstance(cnf_file, str):
|
||||
return
|
||||
self.cnf_file = cnf_file
|
||||
super().__init__()
|
||||
|
||||
def convert_inv(self, conf, inv):
|
||||
if 'serial' in inv:
|
||||
snr = inv['serial']
|
||||
del inv['serial']
|
||||
conf[snr] = {}
|
||||
|
||||
for key, val in inv.items():
|
||||
self._extend_key(conf[snr], key, val)
|
||||
|
||||
def convert_inv_arr(self, conf, key, val: list):
|
||||
if key not in conf:
|
||||
conf[key] = {}
|
||||
for elm in val:
|
||||
self.convert_inv(conf[key], elm)
|
||||
|
||||
def convert_to_obj(self, data):
|
||||
conf = {}
|
||||
for key, val in data.items():
|
||||
if key == 'inverters' and isinstance(val, list):
|
||||
self.convert_inv_arr(conf, key, val)
|
||||
else:
|
||||
self._extend_key(conf, key, val)
|
||||
return conf
|
||||
|
||||
def get_config(self) -> dict:
|
||||
with open(self.cnf_file) as f:
|
||||
data = json.load(f)
|
||||
return self.convert_to_obj(data)
|
||||
|
||||
def descr(self):
|
||||
return self.cnf_file
|
||||
@@ -1,21 +0,0 @@
|
||||
'''Config Reader module which handles *.toml config files'''
|
||||
|
||||
import tomllib
|
||||
from cnf.config import ConfigIfc
|
||||
|
||||
|
||||
class ConfigReadToml(ConfigIfc):
|
||||
'''Reader for toml config files'''
|
||||
def __init__(self, cnf_file):
|
||||
'''Read a toml file and add the settings to the config'''
|
||||
if not isinstance(cnf_file, str):
|
||||
return
|
||||
self.cnf_file = cnf_file
|
||||
super().__init__()
|
||||
|
||||
def get_config(self) -> dict:
|
||||
with open(self.cnf_file, "rb") as f:
|
||||
return tomllib.load(f)
|
||||
|
||||
def descr(self):
|
||||
return self.cnf_file
|
||||
@@ -1,48 +1,19 @@
|
||||
'''Config module handles the proxy configuration'''
|
||||
'''Config module handles the proxy configuration in the config.toml file'''
|
||||
|
||||
import shutil
|
||||
import tomllib
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from schema import Schema, And, Or, Use, Optional
|
||||
|
||||
|
||||
class ConfigIfc(ABC):
|
||||
'''Abstract basis class for config readers'''
|
||||
def __init__(self):
|
||||
Config.add(self)
|
||||
|
||||
@abstractmethod
|
||||
def get_config(self) -> dict: # pragma: no cover
|
||||
'''get the unverified config from the reader'''
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def descr(self) -> str: # pragma: no cover
|
||||
'''return a descriction of the source, e.g. the file name'''
|
||||
pass
|
||||
|
||||
def _extend_key(self, conf, key, val):
|
||||
'''split a dotted dict key into a hierarchical dict tree '''
|
||||
lst = key.split('.')
|
||||
d = conf
|
||||
for i, idx in enumerate(lst, 1): # pragma: no branch
|
||||
if i == len(lst):
|
||||
d[idx] = val
|
||||
break
|
||||
if idx not in d:
|
||||
d[idx] = {}
|
||||
d = d[idx]
|
||||
|
||||
|
||||
class Config():
|
||||
'''Static class Config build and sanitize the internal config dictenary.
|
||||
'''Static class Config is reads and sanitize the config.
|
||||
|
||||
Using config readers, a partial configuration is added to config.
|
||||
Config readers are a derivation of the abstract ConfigIfc reader.
|
||||
When a config reader is instantiated, theits `get_config` method is
|
||||
called automatically and afterwards the config will be merged.
|
||||
'''
|
||||
Read config.toml file and sanitize it with read().
|
||||
Get named parts of the config with get()'''
|
||||
|
||||
act_config = {}
|
||||
def_config = {}
|
||||
conf_schema = Schema({
|
||||
'tsun': {
|
||||
'enabled': Use(bool),
|
||||
@@ -57,10 +28,8 @@ class Config():
|
||||
'mqtt': {
|
||||
'host': Use(str),
|
||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535),
|
||||
'user': Or(None, And(Use(str),
|
||||
Use(lambda s: s if len(s) > 0 else None))),
|
||||
'passwd': Or(None, And(Use(str),
|
||||
Use(lambda s: s if len(s) > 0 else None)))
|
||||
'user': And(Use(str), Use(lambda s: s if len(s) > 0 else None)),
|
||||
'passwd': And(Use(str), Use(lambda s: s if len(s) > 0 else None))
|
||||
},
|
||||
'ha': {
|
||||
'auto_conf_prefix': Use(str),
|
||||
@@ -124,13 +93,7 @@ class Config():
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def init(cls, def_reader: ConfigIfc) -> None | str:
|
||||
'''Initialise the Proxy-Config
|
||||
|
||||
Copy the internal default config file into the config directory
|
||||
and initialise the Config with the default configuration '''
|
||||
cls.err = None
|
||||
cls.def_config = {}
|
||||
def class_init(cls) -> None | str: # pragma: no cover
|
||||
try:
|
||||
# make the default config transparaent by copying it
|
||||
# in the config.example file
|
||||
@@ -140,59 +103,66 @@ and initialise the Config with the default configuration '''
|
||||
"config/config.example.toml")
|
||||
except Exception:
|
||||
pass
|
||||
err_str = cls.read()
|
||||
del cls.conf_schema
|
||||
return err_str
|
||||
|
||||
@classmethod
|
||||
def _read_config_file(cls) -> dict: # pragma: no cover
|
||||
usr_config = {}
|
||||
|
||||
# read example config file as default configuration
|
||||
try:
|
||||
def_config = def_reader.get_config()
|
||||
cls.def_config = cls.conf_schema.validate(def_config)
|
||||
logging.info(f'Read from {def_reader.descr()} => ok')
|
||||
with open("config/config.toml", "rb") as f:
|
||||
usr_config = tomllib.load(f)
|
||||
except Exception as error:
|
||||
cls.err = f'Config.read: {error}'
|
||||
logging.error(
|
||||
f"Can't read from {def_reader.descr()} => error\n {error}")
|
||||
|
||||
cls.act_config = cls.def_config.copy()
|
||||
err = f'Config.read: {error}'
|
||||
logging.error(err)
|
||||
logging.info(
|
||||
'\n To create the missing config.toml file, '
|
||||
'you can rename the template config.example.toml\n'
|
||||
' and customize it for your scenario.\n')
|
||||
return usr_config
|
||||
|
||||
@classmethod
|
||||
def add(cls, reader: ConfigIfc):
|
||||
'''Merge the config from the Config Reader into the config
|
||||
|
||||
Checks if a default config exists. If no default configuration exists,
|
||||
the Config.init method has not yet been called.This is normal for the very
|
||||
first Config Reader which creates the default config and must be ignored
|
||||
here. The default config reader is handled in the Config.init method'''
|
||||
if hasattr(cls, 'def_config'):
|
||||
cls.__parse(reader)
|
||||
|
||||
@classmethod
|
||||
def get_error(cls) -> None | str:
|
||||
'''return the last error as a string or None if there is no error'''
|
||||
return cls.err
|
||||
|
||||
@classmethod
|
||||
def __parse(cls, reader) -> None | str:
|
||||
'''Read config from the reader, merge it with the default config
|
||||
def read(cls, path='') -> None | str:
|
||||
'''Read config file, merge it with the default config
|
||||
and sanitize the result'''
|
||||
res = 'ok'
|
||||
err = None
|
||||
config = {}
|
||||
logger = logging.getLogger('data')
|
||||
|
||||
try:
|
||||
rd_config = reader.get_config()
|
||||
config = cls.act_config.copy()
|
||||
# read example config file as default configuration
|
||||
cls.def_config = {}
|
||||
with open(f"{path}default_config.toml", "rb") as f:
|
||||
def_config = tomllib.load(f)
|
||||
cls.def_config = cls.conf_schema.validate(def_config)
|
||||
|
||||
# overwrite the default values, with values from
|
||||
# the config.toml file
|
||||
usr_config = cls._read_config_file()
|
||||
|
||||
# merge the default and the user config
|
||||
config = def_config.copy()
|
||||
for key in ['tsun', 'solarman', 'mqtt', 'ha', 'inverters',
|
||||
'gen3plus']:
|
||||
if key in rd_config:
|
||||
config[key] = config[key] | rd_config[key]
|
||||
if key in usr_config:
|
||||
config[key] |= usr_config[key]
|
||||
|
||||
try:
|
||||
cls.act_config = cls.conf_schema.validate(config)
|
||||
except Exception as error:
|
||||
err = f'Config.read: {error}'
|
||||
logging.error(err)
|
||||
|
||||
# logging.debug(f'Readed config: "{cls.act_config}" ')
|
||||
|
||||
cls.act_config = cls.conf_schema.validate(config)
|
||||
except FileNotFoundError:
|
||||
res = 'n/a'
|
||||
except Exception as error:
|
||||
cls.err = f'error: {error}'
|
||||
logging.error(
|
||||
f"Can't read from {reader.descr()} => error\n {error}")
|
||||
return cls.err
|
||||
err = f'Config.read: {error}'
|
||||
logger.error(err)
|
||||
cls.act_config = {}
|
||||
|
||||
logging.info(f'Read from {reader.descr()} => {res}')
|
||||
return cls.err
|
||||
return err
|
||||
|
||||
@classmethod
|
||||
def get(cls, member: str = None):
|
||||
@@ -3,7 +3,10 @@ import struct
|
||||
import logging
|
||||
from typing import Generator
|
||||
|
||||
from infos import Infos, Register
|
||||
if __name__ == "app.src.gen3.infos_g3":
|
||||
from app.src.infos import Infos, Register
|
||||
else: # pragma: no cover
|
||||
from infos import Infos, Register
|
||||
|
||||
|
||||
class RegisterMap:
|
||||
@@ -81,7 +84,6 @@ class RegisterMap:
|
||||
0x000012c0: {'reg': Register.RATED_LEVEL},
|
||||
0x00001324: {'reg': Register.INPUT_COEFFICIENT, 'ratio': 100/1024},
|
||||
0x00001388: {'reg': Register.GRID_VOLT_CAL_COEF},
|
||||
0x00002710: {'reg': Register.PROD_COMPL_TYPE},
|
||||
0x00003200: {'reg': Register.OUTPUT_COEFFICIENT, 'ratio': 100/1024},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
|
||||
from inverter_base import InverterBase
|
||||
from gen3.talent import Talent
|
||||
if __name__ == "app.src.gen3.inverter_g3":
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.gen3.talent import Talent
|
||||
else: # pragma: no cover
|
||||
from inverter_base import InverterBase
|
||||
from gen3.talent import Talent
|
||||
|
||||
|
||||
class InverterG3(InverterBase):
|
||||
|
||||
@@ -4,12 +4,20 @@ from zoneinfo import ZoneInfo
|
||||
from datetime import datetime
|
||||
from tzlocal import get_localzone
|
||||
|
||||
from async_ifc import AsyncIfc
|
||||
from messages import Message, State
|
||||
from modbus import Modbus
|
||||
from cnf.config import Config
|
||||
from gen3.infos_g3 import InfosG3
|
||||
from infos import Register
|
||||
if __name__ == "app.src.gen3.talent":
|
||||
from app.src.async_ifc import AsyncIfc
|
||||
from app.src.messages import Message, State
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.config import Config
|
||||
from app.src.gen3.infos_g3 import InfosG3
|
||||
from app.src.infos import Register
|
||||
else: # pragma: no cover
|
||||
from async_ifc import AsyncIfc
|
||||
from messages import Message, State
|
||||
from modbus import Modbus
|
||||
from config import Config
|
||||
from gen3.infos_g3 import InfosG3
|
||||
from infos import Register
|
||||
|
||||
logger = logging.getLogger('msg')
|
||||
|
||||
@@ -178,9 +186,11 @@ class Talent(Message):
|
||||
|
||||
if 2 == (exp_cnt % 30):
|
||||
# logging.info("Regular Modbus Status request")
|
||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.DEBUG)
|
||||
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS, 0x2000,
|
||||
96, logging.DEBUG)
|
||||
else:
|
||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
|
||||
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS, 0x3000,
|
||||
48, logging.DEBUG)
|
||||
|
||||
def _init_new_client_conn(self) -> bool:
|
||||
contact_name = self.contact_name
|
||||
@@ -449,7 +459,7 @@ class Talent(Message):
|
||||
self.__build_header(0x99)
|
||||
self.ifc.tx_add(b'\x01')
|
||||
self.__finish_send_msg()
|
||||
self.__process_data(False)
|
||||
self.__process_data()
|
||||
|
||||
elif self.ctrl.is_resp():
|
||||
return # ignore received response
|
||||
@@ -464,7 +474,7 @@ class Talent(Message):
|
||||
self.__build_header(0x99)
|
||||
self.ifc.tx_add(b'\x01')
|
||||
self.__finish_send_msg()
|
||||
self.__process_data(True)
|
||||
self.__process_data()
|
||||
self.state = State.up # allow MODBUS cmds
|
||||
if (self.modbus_polling):
|
||||
self.mb_timer.start(self.mb_first_timeout)
|
||||
@@ -479,14 +489,8 @@ class Talent(Message):
|
||||
|
||||
self.forward()
|
||||
|
||||
def __process_data(self, ignore_replay: bool):
|
||||
def __process_data(self):
|
||||
msg_hdr_len, ts = self.parse_msg_header()
|
||||
if ignore_replay:
|
||||
age = self._utc() - self._utcfromts(ts)
|
||||
age = age/(3600*24)
|
||||
logger.debug(f"Age: {age} days")
|
||||
if age > 1:
|
||||
return
|
||||
|
||||
for key, update in self.db.parse(self.ifc.rx_peek(), self.header_len
|
||||
+ msg_hdr_len, self.node_id):
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
|
||||
from typing import Generator
|
||||
|
||||
from infos import Infos, Register, ProxyMode, Fmt
|
||||
if __name__ == "app.src.gen3plus.infos_g3p":
|
||||
from app.src.infos import Infos, Register, ProxyMode, Fmt
|
||||
else: # pragma: no cover
|
||||
from infos import Infos, Register, ProxyMode, Fmt
|
||||
|
||||
|
||||
class RegisterMap:
|
||||
@@ -26,11 +29,9 @@ class RegisterMap:
|
||||
0x4102005c: {'reg': None, 'fmt': '<B', 'const': 15}, # noqa: E501
|
||||
0x4102005e: {'reg': None, 'fmt': '<B', 'const': 1}, # noqa: E501 No Of Sensors (ListLen)
|
||||
0x4102005f: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'func': Fmt.hex4}, # noqa: E501
|
||||
0x41020061: {'reg': None, 'fmt': '<HB', 'const': (15, 255)}, # noqa: E501
|
||||
0x41020061: {'reg': None, 'fmt': '<BBB', 'const': (15, 0, 255)}, # noqa: E501
|
||||
0x41020064: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501
|
||||
0x4102008c: {'reg': None, 'fmt': '<BB', 'const': (254, 254)}, # noqa: E501
|
||||
0x4102008e: {'reg': None, 'fmt': '<B'}, # noqa: E501 Encryption Certificate File Status
|
||||
0x4102008f: {'reg': None, 'fmt': '!40s'}, # noqa: E501
|
||||
0x4102008c: {'reg': None, 'fmt': '<BB', 'const': (254, 254)}, # noqa: E501
|
||||
0x410200b7: {'reg': Register.SSID, 'fmt': '!40s'}, # noqa: E501
|
||||
|
||||
0x4201000c: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'func': Fmt.hex4}, # noqa: E501
|
||||
@@ -90,8 +91,7 @@ class RegisterMap:
|
||||
0x4201012c: {'reg': Register.GRID_VOLT_CAL_COEF, 'fmt': '!H'},
|
||||
0x4201012e: {'reg': None, 'fmt': '!H', 'const': 1024}, # noqa: E501
|
||||
0x42010130: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (1024, 1, 0xffff, 1)}, # noqa: E501
|
||||
0x42010138: {'reg': Register.PROD_COMPL_TYPE, 'fmt': '!H'},
|
||||
0x4201013a: {'reg': None, 'fmt': FMT_3_16BIT_VAL, 'const': (0x68, 0x68, 0x500)}, # noqa: E501
|
||||
0x42010138: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (6, 0x68, 0x68, 0x500)}, # noqa: E501
|
||||
0x42010140: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0x9cd, 0x7b6, 0x139c, 0x1324)}, # noqa: E501
|
||||
0x42010148: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (1, 0x7ae, 0x40f, 0x41)}, # noqa: E501
|
||||
0x42010150: {'reg': None, 'fmt': FMT_4_16BIT_VAL, 'const': (0xf, 0xa64, 0xa64, 0x6)}, # noqa: E501
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
|
||||
from inverter_base import InverterBase
|
||||
from gen3plus.solarman_v5 import SolarmanV5
|
||||
from gen3plus.solarman_emu import SolarmanEmu
|
||||
if __name__ == "app.src.gen3plus.inverter_g3p":
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
||||
from app.src.gen3plus.solarman_emu import SolarmanEmu
|
||||
else: # pragma: no cover
|
||||
from inverter_base import InverterBase
|
||||
from gen3plus.solarman_v5 import SolarmanV5
|
||||
from gen3plus.solarman_emu import SolarmanEmu
|
||||
|
||||
|
||||
class InverterG3P(InverterBase):
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from async_ifc import AsyncIfc
|
||||
from gen3plus.solarman_v5 import SolarmanBase
|
||||
from my_timer import Timer
|
||||
from infos import Register
|
||||
if __name__ == "app.src.gen3plus.solarman_emu":
|
||||
from app.src.async_ifc import AsyncIfc
|
||||
from app.src.gen3plus.solarman_v5 import SolarmanBase
|
||||
from app.src.my_timer import Timer
|
||||
from app.src.infos import Register
|
||||
else: # pragma: no cover
|
||||
from async_ifc import AsyncIfc
|
||||
from gen3plus.solarman_v5 import SolarmanBase
|
||||
from my_timer import Timer
|
||||
from infos import Register
|
||||
|
||||
logger = logging.getLogger('msg')
|
||||
|
||||
|
||||
@@ -4,12 +4,20 @@ import time
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
from async_ifc import AsyncIfc
|
||||
from messages import hex_dump_memory, Message, State
|
||||
from cnf.config import Config
|
||||
from modbus import Modbus
|
||||
from gen3plus.infos_g3p import InfosG3P
|
||||
from infos import Register, Fmt
|
||||
if __name__ == "app.src.gen3plus.solarman_v5":
|
||||
from app.src.async_ifc import AsyncIfc
|
||||
from app.src.messages import hex_dump_memory, Message, State
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.config import Config
|
||||
from app.src.gen3plus.infos_g3p import InfosG3P
|
||||
from app.src.infos import Register, Fmt
|
||||
else: # pragma: no cover
|
||||
from async_ifc import AsyncIfc
|
||||
from messages import hex_dump_memory, Message, State
|
||||
from config import Config
|
||||
from modbus import Modbus
|
||||
from gen3plus.infos_g3p import InfosG3P
|
||||
from infos import Register, Fmt
|
||||
|
||||
logger = logging.getLogger('msg')
|
||||
|
||||
@@ -245,7 +253,8 @@ class SolarmanBase(Message):
|
||||
class SolarmanV5(SolarmanBase):
|
||||
AT_CMD = 1
|
||||
MB_RTU_CMD = 2
|
||||
MB_CLIENT_DATA_UP = 30
|
||||
'''regular Modbus polling time in server mode'''
|
||||
MB_CLIENT_DATA_UP = 10
|
||||
'''Data up time in client mode'''
|
||||
HDR_FMT = '<BLLL'
|
||||
'''format string for packing of the header'''
|
||||
@@ -320,7 +329,11 @@ class SolarmanV5(SolarmanBase):
|
||||
if 'at_acl' in g3p_cnf: # pragma: no cover
|
||||
self.at_acl = g3p_cnf['at_acl']
|
||||
|
||||
self.sensor_list = 0
|
||||
self.sensor_list = 0x0000
|
||||
self.mb_start_reg = 0x0001 # 0x7001
|
||||
self.mb_incr_reg = 0x100 # 4
|
||||
self.mb_inv_no = 144 # 3
|
||||
self.mb_scan_len = 4
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
@@ -356,7 +369,23 @@ class SolarmanV5(SolarmanBase):
|
||||
self.new_data['controller'] = True
|
||||
|
||||
self.state = State.up
|
||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
|
||||
# self.__build_header(0x1710)
|
||||
# self.ifc.write += struct.pack('<B', 0)
|
||||
# self.__finish_send_msg()
|
||||
# hex_dump_memory(logging.INFO, f'Send StartCmd:{self.addr}:',
|
||||
# self.ifc.write, len(self.ifc.write))
|
||||
# self.writer.write(self.ifc.write)
|
||||
# self.ifc.write = bytearray(0) # self.ifc.write[sent:]
|
||||
|
||||
if self.sensor_list != 0x02b0:
|
||||
self._send_modbus_cmd(self.mb_inv_no, Modbus.READ_REGS,
|
||||
self.mb_start_reg, self.mb_scan_len,
|
||||
logging.INFO)
|
||||
else:
|
||||
self.mb_inv_no = Modbus.INV_ADDR
|
||||
self._send_modbus_cmd(self.mb_inv_no, Modbus.READ_REGS, 0x3000,
|
||||
48, logging.DEBUG)
|
||||
|
||||
self.mb_timer.start(self.mb_timeout)
|
||||
|
||||
def new_state_up(self):
|
||||
@@ -459,12 +488,29 @@ class SolarmanV5(SolarmanBase):
|
||||
|
||||
def mb_timout_cb(self, exp_cnt):
|
||||
self.mb_timer.start(self.mb_timeout)
|
||||
if self.sensor_list != 0x02b0:
|
||||
self.mb_start_reg += self.mb_incr_reg
|
||||
if self.mb_start_reg > 0xffff:
|
||||
self.mb_start_reg = self.mb_start_reg & 0xffff
|
||||
self.mb_inv_no += 1
|
||||
logging.info(f"Next Round: inv:{self.mb_inv_no}"
|
||||
f" reg:{self.mb_start_reg:04x}")
|
||||
if (self.mb_start_reg & 0xfffc) % 0x80 == 0:
|
||||
logging.info(f"Scan info: inv:{self.mb_inv_no}"
|
||||
f" reg:{self.mb_start_reg:04x}")
|
||||
self._send_modbus_cmd(self.mb_inv_no, Modbus.READ_REGS,
|
||||
self.mb_start_reg, self.mb_scan_len,
|
||||
logging.INFO)
|
||||
else:
|
||||
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS, 0x3000,
|
||||
48, logging.DEBUG)
|
||||
|
||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
|
||||
|
||||
if 1 == (exp_cnt % 30):
|
||||
# logging.info("Regular Modbus Status request")
|
||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.DEBUG)
|
||||
if 1 == (exp_cnt % 30):
|
||||
# logging.info("Regular Modbus Status request")
|
||||
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS,
|
||||
0x5000, 8, logging.DEBUG)
|
||||
self._send_modbus_cmd(Modbus.INV_ADDR, Modbus.READ_REGS,
|
||||
0x2000, 96, logging.DEBUG)
|
||||
|
||||
def at_cmd_forbidden(self, cmd: str, connection: str) -> bool:
|
||||
return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \
|
||||
@@ -677,6 +723,21 @@ class SolarmanV5(SolarmanBase):
|
||||
if valid == 1 and modbus_msg_len > 4:
|
||||
# logger.info(f'first byte modbus:{data[14]}')
|
||||
inv_update = self.__parse_modbus_rsp(data)
|
||||
self.modbus_elms = 0
|
||||
if (self.sensor_list != 0x02b0 and data[15] != 0):
|
||||
logging.info('Valid MODBUS data '
|
||||
f'(inv:{self.mb_inv_no} '
|
||||
f'reg: 0x{self.mb.last_reg:04x}):')
|
||||
hex_dump_memory(logging.INFO, 'Valid MODBUS data '
|
||||
f'(reg: 0x{self.mb.last_reg:04x}):',
|
||||
data[14:], modbus_msg_len)
|
||||
for key, update, _ in self.mb.recv_resp(self.db, data[14:]):
|
||||
self.modbus_elms += 1
|
||||
if update:
|
||||
if key == 'inverter':
|
||||
inv_update = True
|
||||
self._set_mqtt_timestamp(key, self._timestamp())
|
||||
self.new_data[key] = True
|
||||
if inv_update:
|
||||
self.__build_model_name()
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ class Register(Enum):
|
||||
INPUT_COEFFICIENT = 28
|
||||
GRID_VOLT_CAL_COEF = 29
|
||||
OUTPUT_COEFFICIENT = 30
|
||||
PROD_COMPL_TYPE = 31
|
||||
INVERTER_CNT = 50
|
||||
UNKNOWN_SNR = 51
|
||||
UNKNOWN_MSG = 52
|
||||
@@ -299,53 +298,37 @@ class Infos:
|
||||
{% set result = 'noAlarm'%}
|
||||
{%else%}
|
||||
{% set result = '' %}
|
||||
{% if val_int | bitwise_and(1)%}
|
||||
{% set result = result + 'HBridgeFault, '%}
|
||||
{% if val_int | bitwise_and(1)%}{% set result = result + 'Bit1, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(2)%}
|
||||
{% set result = result + 'DriVoltageFault, '%}
|
||||
{% if val_int | bitwise_and(2)%}{% set result = result + 'Bit2, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(3)%}
|
||||
{% set result = result + 'GFDI-Fault, '%}
|
||||
{% if val_int | bitwise_and(3)%}{% set result = result + 'Bit3, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(4)%}
|
||||
{% set result = result + 'OverTemp, '%}
|
||||
{% if val_int | bitwise_and(4)%}{% set result = result + 'Bit4, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(5)%}
|
||||
{% set result = result + 'CommLose, '%}
|
||||
{% if val_int | bitwise_and(5)%}{% set result = result + 'Bit5, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(6)%}
|
||||
{% set result = result + 'Bit6, '%}
|
||||
{% if val_int | bitwise_and(6)%}{% set result = result + 'Bit6, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(7)%}
|
||||
{% set result = result + 'Bit7, '%}
|
||||
{% if val_int | bitwise_and(7)%}{% set result = result + 'Bit7, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(8)%}
|
||||
{% set result = result + 'EEPROM-Fault, '%}
|
||||
{% if val_int | bitwise_and(8)%}{% set result = result + 'Bit8, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(9)%}
|
||||
{% set result = result + 'NoUtility, '%}
|
||||
{% if val_int | bitwise_and(9)%}{% set result = result + 'noUtility, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(10)%}
|
||||
{% set result = result + 'VG_Offset, '%}
|
||||
{% if val_int | bitwise_and(10)%}{% set result = result + 'Bit10, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(11)%}
|
||||
{% set result = result + 'Relais_Open, '%}
|
||||
{% if val_int | bitwise_and(11)%}{% set result = result + 'Bit11, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(12)%}
|
||||
{% set result = result + 'Relais_Short, '%}
|
||||
{% if val_int | bitwise_and(12)%}{% set result = result + 'Bit12, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(13)%}
|
||||
{% set result = result + 'GridVoltOverRating, '%}
|
||||
{% if val_int | bitwise_and(13)%}{% set result = result + 'Bit13, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(14)%}
|
||||
{% set result = result + 'GridVoltUnderRating, '%}
|
||||
{% if val_int | bitwise_and(14)%}{% set result = result + 'Bit14, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(15)%}
|
||||
{% set result = result + 'GridFreqOverRating, '%}
|
||||
{% if val_int | bitwise_and(15)%}{% set result = result + 'Bit15, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(16)%}
|
||||
{% set result = result + 'GridFreqUnderRating, '%}
|
||||
{% if val_int | bitwise_and(16)%}{% set result = result + 'Bit16, '%}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ result }}
|
||||
@@ -361,20 +344,15 @@ class Infos:
|
||||
{% set result = 'noFault'%}
|
||||
{%else%}
|
||||
{% set result = '' %}
|
||||
{% if val_int | bitwise_and(1)%}
|
||||
{% set result = result + 'PVOV-Fault (PV OverVolt), '%}
|
||||
{% if val_int | bitwise_and(1)%}{% set result = result + 'Bit1, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(2)%}
|
||||
{% set result = result + 'PVLV-Fault (PV LowVolt), '%}
|
||||
{% if val_int | bitwise_and(2)%}{% set result = result + 'Bit2, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(3)%}
|
||||
{% set result = result + 'PV OI-Fault (PV OverCurrent), '%}
|
||||
{% if val_int | bitwise_and(3)%}{% set result = result + 'Bit3, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(4)%}
|
||||
{% set result = result + 'PV OFV-Fault, '%}
|
||||
{% if val_int | bitwise_and(4)%}{% set result = result + 'Bit4, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(5)%}
|
||||
{% set result = result + 'DC ShortCircuitFault, '%}
|
||||
{% if val_int | bitwise_and(5)%}{% set result = result + 'Bit5, '%}
|
||||
{% endif %}
|
||||
{% if val_int | bitwise_and(6)%}{% set result = result + 'Bit6, '%}
|
||||
{% endif %}
|
||||
@@ -533,7 +511,6 @@ class Infos:
|
||||
Register.OUTPUT_SHUTDOWN: {'name': ['other', 'Output_Shutdown'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.RATED_LEVEL: {'name': ['other', 'Rated_Level'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.GRID_VOLT_CAL_COEF: {'name': ['other', 'Grid_Volt_Cal_Coef'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PROD_COMPL_TYPE: {'name': ['other', 'Prod_Compliance_Type'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||
Register.INV_UNKNOWN_1: {'name': ['inv_unknown', 'Unknown_1'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
}
|
||||
|
||||
@@ -6,15 +6,23 @@ import json
|
||||
import gc
|
||||
from aiomqtt import MqttCodeError
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from ipaddress import ip_address
|
||||
|
||||
from inverter_ifc import InverterIfc
|
||||
from proxy import Proxy
|
||||
from async_stream import StreamPtr
|
||||
from async_stream import AsyncStreamClient
|
||||
from async_stream import AsyncStreamServer
|
||||
from cnf.config import Config
|
||||
from infos import Infos
|
||||
if __name__ == "app.src.inverter_base":
|
||||
from app.src.inverter_ifc import InverterIfc
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.async_stream import StreamPtr
|
||||
from app.src.async_stream import AsyncStreamClient
|
||||
from app.src.async_stream import AsyncStreamServer
|
||||
from app.src.config import Config
|
||||
from app.src.infos import Infos
|
||||
else: # pragma: no cover
|
||||
from inverter_ifc import InverterIfc
|
||||
from proxy import Proxy
|
||||
from async_stream import StreamPtr
|
||||
from async_stream import AsyncStreamClient
|
||||
from async_stream import AsyncStreamServer
|
||||
from config import Config
|
||||
from infos import Infos
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
@@ -102,20 +110,6 @@ class InverterBase(InverterIfc, Proxy):
|
||||
logging.info(f'[{stream.node_id}] Connect to {addr}')
|
||||
connect = asyncio.open_connection(host, port)
|
||||
reader, writer = await connect
|
||||
r_addr = writer.get_extra_info('peername')
|
||||
if r_addr is not None:
|
||||
(ip, _) = r_addr
|
||||
if ip_address(ip).is_private:
|
||||
logging.error(
|
||||
f"""resolve {host} to {ip}, which is a private IP!
|
||||
\u001B[31m Check your DNS settings and use a public DNS resolver!
|
||||
|
||||
To prevent a possible loop, forwarding to local IP addresses is
|
||||
not supported and is deactivated for subsequent connections
|
||||
\u001B[0m
|
||||
""")
|
||||
Config.act_config[self.config_id]['enabled'] = False
|
||||
|
||||
ifc = AsyncStreamClient(
|
||||
reader, writer, self.local, self.__del_remote)
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ from abc import abstractmethod
|
||||
import logging
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
|
||||
from iter_registry import AbstractIterMeta
|
||||
if __name__ == "app.src.inverter_ifc":
|
||||
from app.src.iter_registry import AbstractIterMeta
|
||||
else: # pragma: no cover
|
||||
from iter_registry import AbstractIterMeta
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
|
||||
@@ -58,13 +58,13 @@ formatter=console_formatter
|
||||
class=handlers.TimedRotatingFileHandler
|
||||
level=INFO
|
||||
formatter=file_formatter
|
||||
args=(handlers.log_path + 'proxy.log', when:='midnight', backupCount:=handlers.log_backups)
|
||||
args=('log/proxy.log', when:='midnight')
|
||||
|
||||
[handler_file_handler_name2]
|
||||
class=handlers.TimedRotatingFileHandler
|
||||
level=NOTSET
|
||||
formatter=file_formatter
|
||||
args=(handlers.log_path + 'trace.log', when:='midnight', backupCount:=handlers.log_backups)
|
||||
args=('log/trace.log', when:='midnight')
|
||||
|
||||
[formatter_console_formatter]
|
||||
format=%(asctime)s %(levelname)5s | %(name)4s | %(message)s'
|
||||
|
||||
@@ -3,11 +3,19 @@ import weakref
|
||||
from typing import Callable
|
||||
from enum import Enum
|
||||
|
||||
from async_ifc import AsyncIfc
|
||||
from protocol_ifc import ProtocolIfc
|
||||
from infos import Infos, Register
|
||||
from modbus import Modbus
|
||||
from my_timer import Timer
|
||||
|
||||
if __name__ == "app.src.messages":
|
||||
from app.src.async_ifc import AsyncIfc
|
||||
from app.src.protocol_ifc import ProtocolIfc
|
||||
from app.src.infos import Infos, Register
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.my_timer import Timer
|
||||
else: # pragma: no cover
|
||||
from async_ifc import AsyncIfc
|
||||
from protocol_ifc import ProtocolIfc
|
||||
from infos import Infos, Register
|
||||
from modbus import Modbus
|
||||
from my_timer import Timer
|
||||
|
||||
logger = logging.getLogger('msg')
|
||||
|
||||
@@ -87,7 +95,7 @@ class Message(ProtocolIfc):
|
||||
'''maximum default time without a received msg in sec'''
|
||||
MB_START_TIMEOUT = 40
|
||||
'''start delay for Modbus polling in server mode'''
|
||||
MB_REGULAR_TIMEOUT = 60
|
||||
MB_REGULAR_TIMEOUT = 20
|
||||
'''regular Modbus polling time in server mode'''
|
||||
|
||||
def __init__(self, node_id, ifc: "AsyncIfc", server_side: bool,
|
||||
@@ -160,15 +168,15 @@ class Message(ProtocolIfc):
|
||||
to = self.MAX_DEF_IDLE_TIME
|
||||
return to
|
||||
|
||||
def _send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
def _send_modbus_cmd(self, mb_no, func, addr, val, log_lvl) -> None:
|
||||
if self.state != State.up:
|
||||
logger.log(log_lvl, f'[{self.node_id}] ignore MODBUS cmd,'
|
||||
' as the state is not UP')
|
||||
return
|
||||
self.mb.build_msg(Modbus.INV_ADDR, func, addr, val, log_lvl)
|
||||
self.mb.build_msg(mb_no, func, addr, val, log_lvl)
|
||||
|
||||
async def send_modbus_cmd(self, func, addr, val, log_lvl) -> None:
|
||||
self._send_modbus_cmd(func, addr, val, log_lvl)
|
||||
self._send_modbus_cmd(Modbus.INV_ADDR, func, addr, val, log_lvl)
|
||||
|
||||
'''
|
||||
Our puplic methods
|
||||
|
||||
@@ -16,7 +16,10 @@ import logging
|
||||
import asyncio
|
||||
from typing import Generator, Callable
|
||||
|
||||
from infos import Register, Fmt
|
||||
if __name__ == "app.src.modbus":
|
||||
from app.src.infos import Register, Fmt
|
||||
else: # pragma: no cover
|
||||
from infos import Register, Fmt
|
||||
|
||||
logger = logging.getLogger('data')
|
||||
|
||||
@@ -41,11 +44,11 @@ class Modbus():
|
||||
0x2001: {'reg': Register.DSP_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
0x2003: {'reg': Register.WORK_MODE, 'fmt': '!H'},
|
||||
0x2006: {'reg': Register.OUTPUT_SHUTDOWN, 'fmt': '!H'},
|
||||
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||
0x2008: {'reg': Register.RATED_LEVEL, 'fmt': '!H'},
|
||||
0x2009: {'reg': Register.INPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
||||
0x200a: {'reg': Register.GRID_VOLT_CAL_COEF, 'fmt': '!H'},
|
||||
0x2010: {'reg': Register.PROD_COMPL_TYPE, 'fmt': '!H'},
|
||||
|
||||
0x202c: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
||||
|
||||
0x3000: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||
@@ -100,8 +103,8 @@ class Modbus():
|
||||
'''Response handler to forward the response'''
|
||||
self.timeout = timeout
|
||||
'''MODBUS response timeout in seconds'''
|
||||
self.max_retries = 1
|
||||
'''Max retransmit for MODBUS requests'''
|
||||
self.max_retries = 0
|
||||
'''Max retransmit for MODBU requests'''
|
||||
self.retry_cnt = 0
|
||||
self.last_req = b''
|
||||
self.counter = {}
|
||||
|
||||
@@ -2,9 +2,14 @@ import logging
|
||||
import traceback
|
||||
import asyncio
|
||||
|
||||
from cnf.config import Config
|
||||
from gen3plus.inverter_g3p import InverterG3P
|
||||
from infos import Infos
|
||||
if __name__ == "app.src.modbus_tcp":
|
||||
from app.src.config import Config
|
||||
from app.src.gen3plus.inverter_g3p import InverterG3P
|
||||
from app.src.infos import Infos
|
||||
else: # pragma: no cover
|
||||
from config import Config
|
||||
from gen3plus.inverter_g3p import InverterG3P
|
||||
from infos import Infos
|
||||
|
||||
logger = logging.getLogger('conn')
|
||||
|
||||
@@ -49,7 +54,7 @@ class ModbusTcp():
|
||||
and 'monitor_sn' in inv
|
||||
and 'client_mode' in inv):
|
||||
client = inv['client_mode']
|
||||
logger.info(f"'client_mode' for snr: {inv['monitor_sn']} host: {client['host']}:{client['port']}, forward: {client['forward']}") # noqa: E501
|
||||
# logging.info(f"SerialNo:{inv['monitor_sn']} host:{client['host']} port:{client['port']}") # noqa: E501
|
||||
loop.create_task(self.modbus_loop(client['host'],
|
||||
client['port'],
|
||||
inv['monitor_sn'],
|
||||
|
||||
@@ -2,11 +2,16 @@ import asyncio
|
||||
import logging
|
||||
import aiomqtt
|
||||
import traceback
|
||||
|
||||
from modbus import Modbus
|
||||
from messages import Message
|
||||
from cnf.config import Config
|
||||
from singleton import Singleton
|
||||
if __name__ == "app.src.mqtt":
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.messages import Message
|
||||
from app.src.config import Config
|
||||
from app.src.singleton import Singleton
|
||||
else: # pragma: no cover
|
||||
from modbus import Modbus
|
||||
from messages import Message
|
||||
from config import Config
|
||||
from singleton import Singleton
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
from async_ifc import AsyncIfc
|
||||
from iter_registry import AbstractIterMeta
|
||||
if __name__ == "app.src.protocol_ifc":
|
||||
from app.src.iter_registry import AbstractIterMeta
|
||||
from app.src.async_ifc import AsyncIfc
|
||||
else: # pragma: no cover
|
||||
from iter_registry import AbstractIterMeta
|
||||
from async_ifc import AsyncIfc
|
||||
|
||||
|
||||
class ProtocolIfc(metaclass=AbstractIterMeta):
|
||||
|
||||
@@ -2,9 +2,14 @@ import asyncio
|
||||
import logging
|
||||
import json
|
||||
|
||||
from cnf.config import Config
|
||||
from mqtt import Mqtt
|
||||
from infos import Infos
|
||||
if __name__ == "app.src.proxy":
|
||||
from app.src.config import Config
|
||||
from app.src.mqtt import Mqtt
|
||||
from app.src.infos import Infos
|
||||
else: # pragma: no cover
|
||||
from config import Config
|
||||
from mqtt import Mqtt
|
||||
from infos import Infos
|
||||
|
||||
logger_mqtt = logging.getLogger('mqtt')
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import logging.handlers
|
||||
import signal
|
||||
import os
|
||||
import argparse
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from aiohttp import web
|
||||
from logging import config # noqa F401
|
||||
@@ -12,10 +10,7 @@ from inverter_ifc import InverterIfc
|
||||
from gen3.inverter_g3 import InverterG3
|
||||
from gen3plus.inverter_g3p import InverterG3P
|
||||
from scheduler import Schedule
|
||||
from cnf.config import Config
|
||||
from cnf.config_read_env import ConfigReadEnv
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
from cnf.config_read_json import ConfigReadJson
|
||||
from config import Config
|
||||
from modbus_tcp import ModbusTcp
|
||||
|
||||
routes = web.RouteTableDef()
|
||||
@@ -82,7 +77,7 @@ async def handle_client(reader: StreamReader, writer: StreamWriter, inv_class):
|
||||
await inv.local.ifc.server_loop()
|
||||
|
||||
|
||||
async def handle_shutdown(loop, web_task):
|
||||
async def handle_shutdown(web_task):
|
||||
'''Close all TCP connections and stop the event loop'''
|
||||
|
||||
logging.info('Shutdown due to SIGTERM')
|
||||
@@ -121,8 +116,6 @@ def get_log_level() -> int:
|
||||
'''checks if LOG_LVL is set in the environment and returns the
|
||||
corresponding logging.LOG_LEVEL'''
|
||||
log_level = os.getenv('LOG_LVL', 'INFO')
|
||||
logging.info(f"LOG_LVL : {log_level}")
|
||||
|
||||
if log_level == 'DEBUG':
|
||||
log_level = logging.DEBUG
|
||||
elif log_level == 'WARN':
|
||||
@@ -132,46 +125,18 @@ def get_log_level() -> int:
|
||||
return log_level
|
||||
|
||||
|
||||
def main(): # pragma: no cover
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-c', '--config_path', type=str,
|
||||
default='./config/',
|
||||
help='set path for the configuration files')
|
||||
parser.add_argument('-j', '--json_config', type=str,
|
||||
help='read user config from json-file')
|
||||
parser.add_argument('-t', '--toml_config', type=str,
|
||||
help='read user config from toml-file')
|
||||
parser.add_argument('-l', '--log_path', type=str,
|
||||
default='./log/',
|
||||
help='set path for the logging files')
|
||||
parser.add_argument('-b', '--log_backups', type=int,
|
||||
default=0,
|
||||
help='set max number of daily log-files')
|
||||
args = parser.parse_args()
|
||||
if __name__ == "__main__":
|
||||
#
|
||||
# Setup our daily, rotating logger
|
||||
#
|
||||
serv_name = os.getenv('SERVICE_NAME', 'proxy')
|
||||
version = os.getenv('VERSION', 'unknown')
|
||||
|
||||
setattr(logging.handlers, "log_path", args.log_path)
|
||||
setattr(logging.handlers, "log_backups", args.log_backups)
|
||||
|
||||
logging.config.fileConfig('logging.ini')
|
||||
logging.info(f'Server "{serv_name} - {version}" will be started')
|
||||
logging.info(f'current dir: {os.getcwd()}')
|
||||
logging.info(f"config_path: {args.config_path}")
|
||||
logging.info(f"json_config: {args.json_config}")
|
||||
logging.info(f"toml_config: {args.toml_config}")
|
||||
logging.info(f"log_path: {args.log_path}")
|
||||
if args.log_backups == 0:
|
||||
logging.info("log_backups: unlimited")
|
||||
else:
|
||||
logging.info(f"log_backups: {args.log_backups} days")
|
||||
log_level = get_log_level()
|
||||
logging.info('******')
|
||||
|
||||
# set lowest-severity for 'root', 'msg', 'conn' and 'data' logger
|
||||
log_level = get_log_level()
|
||||
logging.getLogger().setLevel(log_level)
|
||||
logging.getLogger('msg').setLevel(log_level)
|
||||
logging.getLogger('conn').setLevel(log_level)
|
||||
@@ -184,20 +149,9 @@ def main(): # pragma: no cover
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# read config file
|
||||
Config.init(ConfigReadToml("default_config.toml"))
|
||||
ConfigReadEnv()
|
||||
ConfigReadJson(args.config_path + "config.json")
|
||||
ConfigReadToml(args.config_path + "config.toml")
|
||||
ConfigReadJson(args.json_config)
|
||||
ConfigReadToml(args.toml_config)
|
||||
config_err = Config.get_error()
|
||||
|
||||
if config_err is not None:
|
||||
logging.info(f'config_err: {config_err}')
|
||||
return
|
||||
|
||||
logging.info('******')
|
||||
|
||||
ConfigErr = Config.class_init()
|
||||
if ConfigErr is not None:
|
||||
logging.info(f'ConfigErr: {ConfigErr}')
|
||||
Proxy.class_init()
|
||||
Schedule.start()
|
||||
ModbusTcp(loop)
|
||||
@@ -208,7 +162,6 @@ def main(): # pragma: no cover
|
||||
# and we can't receive and handle the UNIX signals!
|
||||
#
|
||||
for inv_class, port in [(InverterG3, 5005), (InverterG3P, 10000)]:
|
||||
logging.info(f'listen on port: {port} for inverters')
|
||||
loop.create_task(asyncio.start_server(lambda r, w, i=inv_class:
|
||||
handle_client(r, w, i),
|
||||
'0.0.0.0', port))
|
||||
@@ -221,12 +174,12 @@ def main(): # pragma: no cover
|
||||
for signame in ('SIGINT', 'SIGTERM'):
|
||||
loop.add_signal_handler(getattr(signal, signame),
|
||||
lambda loop=loop: asyncio.create_task(
|
||||
handle_shutdown(loop, web_task)))
|
||||
handle_shutdown(web_task)))
|
||||
|
||||
loop.set_debug(log_level == logging.DEBUG)
|
||||
try:
|
||||
global proxy_is_up
|
||||
proxy_is_up = True
|
||||
if ConfigErr is None:
|
||||
proxy_is_up = True
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
@@ -236,7 +189,3 @@ def main(): # pragma: no cover
|
||||
logging.debug('Close event loop')
|
||||
loop.close()
|
||||
logging.info(f'Finally, exit Server "{serv_name}"')
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
|
||||
@@ -4,13 +4,12 @@ import asyncio
|
||||
import gc
|
||||
import time
|
||||
|
||||
from infos import Infos
|
||||
from inverter_base import InverterBase
|
||||
from async_stream import AsyncStreamServer, AsyncStreamClient, StreamPtr
|
||||
from messages import Message
|
||||
|
||||
from test_modbus_tcp import FakeReader, FakeWriter
|
||||
from test_inverter_base import config_conn, patch_open_connection
|
||||
from app.src.infos import Infos
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.async_stream import AsyncStreamServer, AsyncStreamClient, StreamPtr
|
||||
from app.src.messages import Message
|
||||
from app.tests.test_modbus_tcp import FakeReader, FakeWriter
|
||||
from app.tests.test_inverter_base import config_conn, patch_open_connection
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -542,7 +541,7 @@ async def test_forward_resp():
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
def _close_cb():
|
||||
async def _close_cb():
|
||||
nonlocal cnt, remote, ifc
|
||||
cnt += 1
|
||||
|
||||
@@ -551,7 +550,7 @@ async def test_forward_resp():
|
||||
create_remote(remote, TestType.FWD_NO_EXCPT)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.client_loop('')
|
||||
assert cnt == 1
|
||||
assert cnt == 0
|
||||
del ifc
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -560,7 +559,7 @@ async def test_forward_resp2():
|
||||
remote = StreamPtr(None)
|
||||
cnt = 0
|
||||
|
||||
def _close_cb():
|
||||
async def _close_cb():
|
||||
nonlocal cnt, remote, ifc
|
||||
cnt += 1
|
||||
|
||||
@@ -569,5 +568,5 @@ async def test_forward_resp2():
|
||||
create_remote(remote, TestType.FWD_NO_EXCPT)
|
||||
ifc.fwd_add(b'test-forward_msg')
|
||||
await ifc.client_loop('')
|
||||
assert cnt == 1
|
||||
assert cnt == 0
|
||||
del ifc
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# test_with_pytest.py
|
||||
|
||||
from byte_fifo import ByteFifo
|
||||
from app.src.byte_fifo import ByteFifo
|
||||
|
||||
def test_fifo():
|
||||
read = ByteFifo()
|
||||
|
||||
@@ -1,56 +1,16 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import json
|
||||
from mock import patch
|
||||
import tomllib
|
||||
from schema import SchemaMissingKeyError
|
||||
from cnf.config import Config, ConfigIfc
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
from app.src.config import Config
|
||||
|
||||
class FakeBuffer:
|
||||
rd = str()
|
||||
|
||||
test_buffer = FakeBuffer
|
||||
|
||||
|
||||
class FakeFile():
|
||||
def __init__(self):
|
||||
self.buf = test_buffer
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
|
||||
class FakeOptionsFile(FakeFile):
|
||||
def __init__(self, OpenTextMode):
|
||||
super().__init__()
|
||||
self.bin_mode = 'b' in OpenTextMode
|
||||
|
||||
def read(self):
|
||||
if self.bin_mode:
|
||||
return bytearray(self.buf.rd.encode('utf-8')).copy()
|
||||
else:
|
||||
return self.buf.rd.copy()
|
||||
|
||||
def patch_open():
|
||||
def new_open(file: str, OpenTextMode="rb"):
|
||||
if file == "_no__file__no_":
|
||||
raise FileNotFoundError
|
||||
return FakeOptionsFile(OpenTextMode)
|
||||
|
||||
with patch('builtins.open', new_open) as conn:
|
||||
yield conn
|
||||
|
||||
class TstConfig(ConfigIfc):
|
||||
class TstConfig(Config):
|
||||
|
||||
@classmethod
|
||||
def __init__(cls, cnf):
|
||||
def set(cls, cnf):
|
||||
cls.act_config = cnf
|
||||
|
||||
@classmethod
|
||||
def add_config(cls) -> dict:
|
||||
def _read_config_file(cls) -> dict:
|
||||
return cls.act_config
|
||||
|
||||
|
||||
@@ -62,90 +22,14 @@ def test_empty_config():
|
||||
except SchemaMissingKeyError:
|
||||
pass
|
||||
|
||||
@pytest.fixture
|
||||
def ConfigDefault():
|
||||
return {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
'R170000000000001': {
|
||||
'suggested_area': '',
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'sensor_list': 688
|
||||
},
|
||||
'Y170000000000001': {
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'suggested_area': '',
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv3': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv4': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'sensor_list': 688
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def ConfigComplete():
|
||||
return {
|
||||
'gen3plus': {
|
||||
'at_acl': {
|
||||
'mqtt': {'allow': ['AT+'], 'block': ['AT+SUPDATE']},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'],
|
||||
'block': ['AT+SUPDATE']}
|
||||
}
|
||||
},
|
||||
'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com',
|
||||
'port': 5005},
|
||||
'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com',
|
||||
'port': 10000},
|
||||
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None},
|
||||
'ha': {'auto_conf_prefix': 'homeassistant',
|
||||
'discovery_prefix': 'homeassistant',
|
||||
'entity_prefix': 'tsun',
|
||||
'proxy_node_id': 'proxy',
|
||||
'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
'R170000000000001': {'node_id': 'PV-Garage/',
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'pv1': {'manufacturer': 'man1',
|
||||
'type': 'type1'},
|
||||
'pv2': {'manufacturer': 'man2',
|
||||
'type': 'type2'},
|
||||
'suggested_area': 'Garage',
|
||||
'sensor_list': 688},
|
||||
'Y170000000000001': {'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'node_id': 'PV-Garage2/',
|
||||
'pv1': {'manufacturer': 'man1',
|
||||
'type': 'type1'},
|
||||
'pv2': {'manufacturer': 'man2',
|
||||
'type': 'type2'},
|
||||
'pv3': {'manufacturer': 'man3',
|
||||
'type': 'type3'},
|
||||
'pv4': {'manufacturer': 'man4',
|
||||
'type': 'type4'},
|
||||
'suggested_area': 'Garage2',
|
||||
'sensor_list': 688}
|
||||
}
|
||||
}
|
||||
|
||||
def test_default_config():
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
validated = Config.def_config
|
||||
with open("app/config/default_config.toml", "rb") as f:
|
||||
cnf = tomllib.load(f)
|
||||
|
||||
try:
|
||||
validated = Config.conf_schema.validate(cnf)
|
||||
except Exception:
|
||||
assert False
|
||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
@@ -174,118 +58,45 @@ def test_default_config():
|
||||
'suggested_area': '',
|
||||
'sensor_list': 688}}}
|
||||
|
||||
def test_full_config(ConfigComplete):
|
||||
def test_full_config():
|
||||
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
||||
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': ['AT+SUPDATE']},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': ['AT+SUPDATE']}}},
|
||||
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}},
|
||||
'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000},
|
||||
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
|
||||
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {'allow_all': False,
|
||||
'R170000000000001': {'modbus_polling': False, 'node_id': 'PV-Garage/', 'sensor_list': 0x02B0, 'suggested_area': 'Garage', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}},
|
||||
'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': 'PV-Garage2/', 'sensor_list': 0x02B0, 'suggested_area': 'Garage2', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}, 'pv3': {'type': 'type3', 'manufacturer': 'man3'}, 'pv4': {'type': 'type4', 'manufacturer': 'man4'}}}}
|
||||
'inverters': {'allow_all': True,
|
||||
'R170000000000001': {'modbus_polling': True, 'node_id': '', 'sensor_list': 0, 'suggested_area': '', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}, 'pv3': {'type': 'type3', 'manufacturer': 'man3'}},
|
||||
'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'sensor_list': 0x1511, 'suggested_area': ''}}}
|
||||
try:
|
||||
validated = Config.conf_schema.validate(cnf)
|
||||
except Exception:
|
||||
assert False
|
||||
assert validated == ConfigComplete
|
||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'modbus_polling': True, 'monitor_sn': 0, 'pv1': {'manufacturer': 'man1','type': 'type1'},'pv2': {'manufacturer': 'man2','type': 'type2'},'pv3': {'manufacturer': 'man3','type': 'type3'}, 'suggested_area': '', 'sensor_list': 0}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': '', 'sensor_list': 5393}}}
|
||||
|
||||
def test_read_empty(ConfigDefault):
|
||||
test_buffer.rd = ""
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
def test_mininum_config():
|
||||
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
||||
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+']},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE']}}},
|
||||
'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000},
|
||||
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
|
||||
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {'allow_all': True,
|
||||
'R170000000000001': {}}
|
||||
}
|
||||
|
||||
try:
|
||||
validated = Config.conf_schema.validate(cnf)
|
||||
except Exception:
|
||||
assert False
|
||||
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'modbus_polling': True, 'monitor_sn': 0, 'suggested_area': '', 'sensor_list': 688}}}
|
||||
|
||||
def test_read_empty():
|
||||
cnf = {}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
defcnf = Config.def_config.get('solarman')
|
||||
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||
assert True == Config.is_default('solarman')
|
||||
|
||||
def test_no_file():
|
||||
Config.init(ConfigReadToml("default_config.toml"))
|
||||
err = Config.get_error()
|
||||
assert err == "Config.read: [Errno 2] No such file or directory: 'default_config.toml'"
|
||||
cnf = Config.get()
|
||||
assert cnf == {}
|
||||
defcnf = Config.def_config.get('solarman')
|
||||
assert defcnf == None
|
||||
|
||||
def test_no_file2():
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
assert Config.err == None
|
||||
ConfigReadToml("_no__file__no_")
|
||||
err = Config.get_error()
|
||||
assert err == None
|
||||
|
||||
def test_invalid_filename():
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
assert Config.err == None
|
||||
ConfigReadToml(None)
|
||||
err = Config.get_error()
|
||||
assert err == None
|
||||
|
||||
def test_read_cnf1():
|
||||
test_buffer.rd = "solarman.enabled = false"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
'R170000000000001': {
|
||||
'suggested_area': '',
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'sensor_list': 688
|
||||
},
|
||||
'Y170000000000001': {
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'suggested_area': '',
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv3': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv4': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'sensor_list': 688
|
||||
}
|
||||
}
|
||||
}
|
||||
cnf = Config.get('solarman')
|
||||
assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||
defcnf = Config.def_config.get('solarman')
|
||||
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||
assert False == Config.is_default('solarman')
|
||||
|
||||
def test_read_cnf2():
|
||||
test_buffer.rd = "solarman.enabled = 'FALSE'"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
cnf = TstConfig.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
@@ -317,30 +128,117 @@ def test_read_cnf2():
|
||||
}
|
||||
}
|
||||
}
|
||||
assert True == Config.is_default('solarman')
|
||||
|
||||
def test_read_cnf3(ConfigDefault):
|
||||
test_buffer.rd = "solarman.port = 'FALSE'"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
defcnf = TstConfig.def_config.get('solarman')
|
||||
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||
assert True == TstConfig.is_default('solarman')
|
||||
|
||||
assert err == 'error: Key \'solarman\' error:\nKey \'port\' error:\nint(\'FALSE\') raised ValueError("invalid literal for int() with base 10: \'FALSE\'")'
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
def test_no_file():
|
||||
cnf = {}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('')
|
||||
assert err == "Config.read: [Errno 2] No such file or directory: 'default_config.toml'"
|
||||
cnf = TstConfig.get()
|
||||
assert cnf == {}
|
||||
defcnf = TstConfig.def_config.get('solarman')
|
||||
assert defcnf == None
|
||||
|
||||
def test_read_cnf1():
|
||||
cnf = {'solarman' : {'enabled': False}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
assert err == None
|
||||
cnf = TstConfig.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
'R170000000000001': {
|
||||
'suggested_area': '',
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'sensor_list': 688
|
||||
},
|
||||
'Y170000000000001': {
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'suggested_area': '',
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv3': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv4': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'sensor_list': 688
|
||||
}
|
||||
}
|
||||
}
|
||||
cnf = TstConfig.get('solarman')
|
||||
assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||
defcnf = TstConfig.def_config.get('solarman')
|
||||
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||
assert False == TstConfig.is_default('solarman')
|
||||
|
||||
def test_read_cnf2():
|
||||
cnf = {'solarman' : {'enabled': 'FALSE'}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
assert err == None
|
||||
cnf = TstConfig.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
'R170000000000001': {
|
||||
'suggested_area': '',
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'sensor_list': 688
|
||||
},
|
||||
'Y170000000000001': {
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'suggested_area': '',
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv3': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'pv4': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-410M'},
|
||||
'sensor_list': 688
|
||||
}
|
||||
}
|
||||
}
|
||||
assert True == TstConfig.is_default('solarman')
|
||||
|
||||
def test_read_cnf3():
|
||||
cnf = {'solarman' : {'port': 'FALSE'}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
assert err == 'Config.read: Key \'solarman\' error:\nKey \'port\' error:\nint(\'FALSE\') raised ValueError("invalid literal for int() with base 10: \'FALSE\'")'
|
||||
cnf = TstConfig.get()
|
||||
assert cnf == {'solarman': {'port': 'FALSE'}}
|
||||
|
||||
def test_read_cnf4():
|
||||
test_buffer.rd = "solarman.port = 5000"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
cnf = {'solarman' : {'port': 5000}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
cnf = TstConfig.get()
|
||||
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 5000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||
'inverters': {
|
||||
'allow_all': False,
|
||||
@@ -372,22 +270,16 @@ def test_read_cnf4():
|
||||
}
|
||||
}
|
||||
}
|
||||
assert False == Config.is_default('solarman')
|
||||
assert False == TstConfig.is_default('solarman')
|
||||
|
||||
def test_read_cnf5():
|
||||
test_buffer.rd = "solarman.port = 1023"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
cnf = {'solarman' : {'port': 1023}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
assert err != None
|
||||
|
||||
def test_read_cnf6():
|
||||
test_buffer.rd = "solarman.port = 65536"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
cnf = {'solarman' : {'port': 65536}}
|
||||
TstConfig.set(cnf)
|
||||
err = TstConfig.read('app/config/')
|
||||
assert err != None
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import os
|
||||
from mock import patch
|
||||
from cnf.config import Config
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
from cnf.config_read_env import ConfigReadEnv
|
||||
|
||||
def patch_getenv():
|
||||
def new_getenv(key: str, defval=None):
|
||||
"""Get an environment variable, return None if it doesn't exist.
|
||||
The optional second argument can specify an alternate default. key,
|
||||
default and the result are str."""
|
||||
if key == 'MQTT_PASSWORD':
|
||||
return 'passwd'
|
||||
elif key == 'MQTT_PORT':
|
||||
return 1234
|
||||
elif key == 'MQTT_HOST':
|
||||
return ""
|
||||
return defval
|
||||
|
||||
with patch.object(os, 'getenv', new_getenv) as conn:
|
||||
yield conn
|
||||
|
||||
def test_extend_key():
|
||||
cnf_rd = ConfigReadEnv()
|
||||
|
||||
conf = {}
|
||||
cnf_rd._extend_key(conf, "mqtt.user", "testuser")
|
||||
assert conf == {
|
||||
'mqtt': {
|
||||
'user': 'testuser',
|
||||
},
|
||||
}
|
||||
|
||||
conf = {}
|
||||
cnf_rd._extend_key(conf, "mqtt", "testuser")
|
||||
assert conf == {
|
||||
'mqtt': 'testuser',
|
||||
}
|
||||
|
||||
conf = {}
|
||||
cnf_rd._extend_key(conf, "", "testuser")
|
||||
assert conf == {'': 'testuser'}
|
||||
|
||||
def test_read_env_config():
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
assert Config.get('mqtt') == {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}
|
||||
for _ in patch_getenv():
|
||||
|
||||
ConfigReadEnv()
|
||||
assert Config.get_error() == None
|
||||
assert Config.get('mqtt') == {'host': 'mqtt', 'port': 1234, 'user': None, 'passwd': 'passwd'}
|
||||
@@ -1,411 +0,0 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
from mock import patch
|
||||
from cnf.config import Config
|
||||
from cnf.config_read_json import ConfigReadJson
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
|
||||
from test_config import ConfigDefault, ConfigComplete
|
||||
|
||||
|
||||
class CnfIfc(ConfigReadJson):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
class FakeBuffer:
|
||||
rd = str()
|
||||
wr = str()
|
||||
|
||||
|
||||
test_buffer = FakeBuffer
|
||||
|
||||
|
||||
class FakeFile():
|
||||
def __init__(self):
|
||||
self.buf = test_buffer
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
|
||||
class FakeOptionsFile(FakeFile):
|
||||
def __init__(self, OpenTextMode):
|
||||
super().__init__()
|
||||
self.bin_mode = 'b' in OpenTextMode
|
||||
|
||||
def read(self):
|
||||
print(f"Fake.read: bmode:{self.bin_mode}")
|
||||
if self.bin_mode:
|
||||
return bytearray(self.buf.rd.encode('utf-8')).copy()
|
||||
else:
|
||||
print(f"Fake.read: str:{self.buf.rd}")
|
||||
return self.buf.rd
|
||||
|
||||
def patch_open():
|
||||
def new_open(file: str, OpenTextMode="r"):
|
||||
if file == "_no__file__no_":
|
||||
raise FileNotFoundError
|
||||
return FakeOptionsFile(OpenTextMode)
|
||||
|
||||
with patch('builtins.open', new_open) as conn:
|
||||
yield conn
|
||||
|
||||
@pytest.fixture
|
||||
def ConfigTomlEmpty():
|
||||
return {
|
||||
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
|
||||
'ha': {'auto_conf_prefix': 'homeassistant',
|
||||
'discovery_prefix': 'homeassistant',
|
||||
'entity_prefix': 'tsun',
|
||||
'proxy_node_id': 'proxy',
|
||||
'proxy_unique_id': 'P170000000000001'},
|
||||
'solarman': {
|
||||
'enabled': True,
|
||||
'host': 'iot.talent-monitoring.com',
|
||||
'port': 10000,
|
||||
},
|
||||
'tsun': {
|
||||
'enabled': True,
|
||||
'host': 'logger.talent-monitoring.com',
|
||||
'port': 5005,
|
||||
},
|
||||
'inverters': {
|
||||
'allow_all': False
|
||||
},
|
||||
'gen3plus': {'at_acl': {'tsun': {'allow': [], 'block': []},
|
||||
'mqtt': {'allow': [], 'block': []}}},
|
||||
}
|
||||
|
||||
|
||||
def test_no_config(ConfigDefault):
|
||||
test_buffer.rd = "" # empty buffer, no json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson()
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == 'error: Expecting value: line 1 column 1 (char 0)'
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
def test_no_file(ConfigDefault):
|
||||
test_buffer.rd = "" # empty buffer, no json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson("_no__file__no_")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
def test_invalid_filename(ConfigDefault):
|
||||
test_buffer.rd = "" # empty buffer, no json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson(None)
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
def test_cnv1():
|
||||
"""test dotted key converting"""
|
||||
tst = {
|
||||
"gen3plus.at_acl.mqtt.block": [
|
||||
"AT+SUPDATE",
|
||||
"AT+"
|
||||
]
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'gen3plus': {
|
||||
'at_acl': {
|
||||
'mqtt': {
|
||||
'block': [
|
||||
'AT+SUPDATE',
|
||||
"AT+"
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv2():
|
||||
"""test a valid list with serials in inverters"""
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'inverters': {
|
||||
'R170000000000001': {},
|
||||
'Y170000000000001': {}
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv3():
|
||||
"""test the combination of a list and a scalar in inverters"""
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
}
|
||||
],
|
||||
"inverters.allow_all": False,
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'inverters': {
|
||||
'R170000000000001': {},
|
||||
'Y170000000000001': {},
|
||||
'allow_all': False,
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv4():
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
"node_id": "PV-Garage/",
|
||||
"suggested_area": "Garage",
|
||||
"modbus_polling": False,
|
||||
"pv1.manufacturer": "man1",
|
||||
"pv1.type": "type1",
|
||||
"pv2.manufacturer": "man2",
|
||||
"pv2.type": "type2",
|
||||
"sensor_list": 688
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
"monitor_sn": 2000000000,
|
||||
"node_id": "PV-Garage2/",
|
||||
"suggested_area": "Garage2",
|
||||
"modbus_polling": True,
|
||||
"client_mode.host": "InverterIP",
|
||||
"client_mode.port": 1234,
|
||||
"client_mode.forward": True,
|
||||
"pv1.manufacturer": "man1",
|
||||
"pv1.type": "type1",
|
||||
"pv2.manufacturer": "man2",
|
||||
"pv2.type": "type2",
|
||||
"pv3.manufacturer": "man3",
|
||||
"pv3.type": "type3",
|
||||
"pv4.manufacturer": "man4",
|
||||
"pv4.type": "type4",
|
||||
"sensor_list": 688
|
||||
}
|
||||
],
|
||||
"tsun.enabled": True,
|
||||
"solarman.enabled": True,
|
||||
"inverters.allow_all": False,
|
||||
"gen3plus.at_acl.tsun.allow": [
|
||||
"AT+Z",
|
||||
"AT+UPURL",
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.tsun.block": [
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.allow": [
|
||||
"AT+"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.block": [
|
||||
"AT+SUPDATE"
|
||||
]
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': ['AT+SUPDATE']},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'],
|
||||
'block': ['AT+SUPDATE']}}},
|
||||
'inverters': {'R170000000000001': {'modbus_polling': False,
|
||||
'node_id': 'PV-Garage/',
|
||||
'pv1': {
|
||||
'manufacturer': 'man1',
|
||||
'type': 'type1'},
|
||||
'pv2': {
|
||||
'manufacturer': 'man2',
|
||||
'type': 'type2'},
|
||||
'sensor_list': 688,
|
||||
'suggested_area': 'Garage'},
|
||||
'Y170000000000001': {'client_mode': {
|
||||
'host': 'InverterIP',
|
||||
'port': 1234,
|
||||
'forward': True},
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'node_id': 'PV-Garage2/',
|
||||
'pv1': {
|
||||
'manufacturer': 'man1',
|
||||
'type': 'type1'},
|
||||
'pv2': {
|
||||
'manufacturer': 'man2',
|
||||
'type': 'type2'},
|
||||
'pv3': {
|
||||
'manufacturer': 'man3',
|
||||
'type': 'type3'},
|
||||
'pv4': {
|
||||
'manufacturer': 'man4',
|
||||
'type': 'type4'},
|
||||
'sensor_list': 688,
|
||||
'suggested_area': 'Garage2'},
|
||||
'allow_all': False},
|
||||
'solarman': {'enabled': True},
|
||||
'tsun': {'enabled': True}
|
||||
}
|
||||
|
||||
def test_cnv5():
|
||||
"""test a invalid list with missing serials"""
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"node_id": "PV-Garage1/",
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
"node_id": "PV-Garage2/",
|
||||
}
|
||||
],
|
||||
}
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'inverters': {
|
||||
'Y170000000000001': {'node_id': 'PV-Garage2/'}
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv6():
|
||||
"""test overwritting a value in inverters"""
|
||||
tst = {
|
||||
"inverters": [{
|
||||
"serial": "Y170000000000001",
|
||||
"node_id": "PV-Garage2/",
|
||||
}],
|
||||
}
|
||||
tst2 = {
|
||||
"inverters": [{
|
||||
"serial": "Y170000000000001",
|
||||
"node_id": "PV-Garden/",
|
||||
}],
|
||||
}
|
||||
cnf = ConfigReadJson()
|
||||
conf = {}
|
||||
for key, val in tst.items():
|
||||
cnf.convert_inv_arr(conf, key, val)
|
||||
|
||||
assert conf == {
|
||||
'inverters': {
|
||||
'Y170000000000001': {'node_id': 'PV-Garage2/'}
|
||||
},
|
||||
}
|
||||
|
||||
for key, val in tst2.items():
|
||||
cnf.convert_inv_arr(conf, key, val)
|
||||
|
||||
assert conf == {
|
||||
'inverters': {
|
||||
'Y170000000000001': {'node_id': 'PV-Garden/'}
|
||||
},
|
||||
}
|
||||
|
||||
def test_empty_config(ConfigDefault):
|
||||
test_buffer.rd = "{}" # empty json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson()
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
|
||||
def test_full_config(ConfigComplete):
|
||||
test_buffer.rd = """
|
||||
{
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
"node_id": "PV-Garage/",
|
||||
"suggested_area": "Garage",
|
||||
"modbus_polling": false,
|
||||
"pv1.manufacturer": "man1",
|
||||
"pv1.type": "type1",
|
||||
"pv2.manufacturer": "man2",
|
||||
"pv2.type": "type2",
|
||||
"sensor_list": 688
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
"monitor_sn": 2000000000,
|
||||
"node_id": "PV-Garage2/",
|
||||
"suggested_area": "Garage2",
|
||||
"modbus_polling": true,
|
||||
"pv1.manufacturer": "man1",
|
||||
"pv1.type": "type1",
|
||||
"pv2.manufacturer": "man2",
|
||||
"pv2.type": "type2",
|
||||
"pv3.manufacturer": "man3",
|
||||
"pv3.type": "type3",
|
||||
"pv4.manufacturer": "man4",
|
||||
"pv4.type": "type4",
|
||||
"sensor_list": 688
|
||||
}
|
||||
],
|
||||
"tsun.enabled": true,
|
||||
"solarman.enabled": true,
|
||||
"inverters.allow_all": false,
|
||||
"gen3plus.at_acl.tsun.allow": [
|
||||
"AT+Z",
|
||||
"AT+UPURL",
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.tsun.block": [
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.allow": [
|
||||
"AT+"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.block": [
|
||||
"AT+SUPDATE"
|
||||
]
|
||||
}
|
||||
"""
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson()
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigComplete
|
||||
@@ -2,8 +2,8 @@
|
||||
import pytest
|
||||
import json, math
|
||||
import logging
|
||||
from infos import Register, ClrAtMidnight
|
||||
from infos import Infos, Fmt
|
||||
from app.src.infos import Register, ClrAtMidnight
|
||||
from app.src.infos import Infos, Fmt
|
||||
|
||||
def test_statistic_counter():
|
||||
i = Infos()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# test_with_pytest.py
|
||||
import pytest, json, math
|
||||
from infos import Register
|
||||
from gen3.infos_g3 import InfosG3, RegisterMap
|
||||
from app.src.infos import Register
|
||||
from app.src.gen3.infos_g3 import InfosG3, RegisterMap
|
||||
|
||||
@pytest.fixture
|
||||
def contr_data_seq(): # Get Time Request message
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
|
||||
# test_with_pytest.py
|
||||
import pytest, json, math, random
|
||||
from infos import Register
|
||||
from gen3plus.infos_g3p import InfosG3P
|
||||
from gen3plus.infos_g3p import RegisterMap
|
||||
from app.src.infos import Register
|
||||
from app.src.gen3plus.infos_g3p import InfosG3P
|
||||
from app.src.gen3plus.infos_g3p import RegisterMap
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def str_test_ip():
|
||||
@@ -120,7 +120,7 @@ def test_parse_4210(inverter_data: bytes):
|
||||
"pv4": {"Voltage": 1.7, "Current": 0.01, "Power": 0.0, "Total_Generation": 15.58}},
|
||||
"total": {"Daily_Generation": 0.11, "Total_Generation": 101.36},
|
||||
"inv_unknown": {"Unknown_1": 512},
|
||||
"other": {"Output_Shutdown": 65535, "Rated_Level": 3, "Grid_Volt_Cal_Coef": 1024, "Prod_Compliance_Type": 6}
|
||||
"other": {"Output_Shutdown": 65535, "Rated_Level": 3, "Grid_Volt_Cal_Coef": 1024}
|
||||
})
|
||||
|
||||
def test_build_4210(inverter_data: bytes):
|
||||
|
||||
@@ -5,14 +5,14 @@ import gc
|
||||
|
||||
from mock import patch
|
||||
from enum import Enum
|
||||
from infos import Infos
|
||||
from cnf.config import Config
|
||||
from gen3.talent import Talent
|
||||
from inverter_base import InverterBase
|
||||
from singleton import Singleton
|
||||
from async_stream import AsyncStream, AsyncStreamClient
|
||||
from app.src.infos import Infos
|
||||
from app.src.config import Config
|
||||
from app.src.gen3.talent import Talent
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.async_stream import AsyncStream, AsyncStreamClient
|
||||
|
||||
from test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
from app.tests.test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -54,12 +54,11 @@ class FakeReader():
|
||||
|
||||
|
||||
class FakeWriter():
|
||||
peer = ('47.1.2.3', 10000)
|
||||
def write(self, buf: bytes):
|
||||
return
|
||||
def get_extra_info(self, sel: str):
|
||||
if sel == 'peername':
|
||||
return self.peer
|
||||
return 'remote.intern'
|
||||
elif sel == 'sockname':
|
||||
return 'sock:1234'
|
||||
assert False
|
||||
@@ -70,13 +69,13 @@ class FakeWriter():
|
||||
async def wait_closed(self):
|
||||
return
|
||||
|
||||
class MockType(Enum):
|
||||
class TestType(Enum):
|
||||
RD_TEST_0_BYTES = 1
|
||||
RD_TEST_TIMEOUT = 2
|
||||
RD_TEST_EXCEPT = 3
|
||||
|
||||
|
||||
test = MockType.RD_TEST_0_BYTES
|
||||
test = TestType.RD_TEST_0_BYTES
|
||||
|
||||
@pytest.fixture
|
||||
def patch_open_connection():
|
||||
@@ -86,9 +85,9 @@ def patch_open_connection():
|
||||
|
||||
def new_open(host: str, port: int):
|
||||
global test
|
||||
if test == MockType.RD_TEST_TIMEOUT:
|
||||
if test == TestType.RD_TEST_TIMEOUT:
|
||||
raise ConnectionRefusedError
|
||||
elif test == MockType.RD_TEST_EXCEPT:
|
||||
elif test == TestType.RD_TEST_EXCEPT:
|
||||
raise ValueError("Value cannot be negative") # Compliant
|
||||
return new_conn(None)
|
||||
|
||||
@@ -242,118 +241,6 @@ async def test_remote_conn(config_conn, patch_open_connection):
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remote_conn_to_private(config_conn, patch_open_connection):
|
||||
'''check DNS resolving of the TSUN FQDN to a local address'''
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
assert asyncio.get_running_loop()
|
||||
InverterBase._registry.clear()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
FakeWriter.peer = ("192.168.0.1", 10000)
|
||||
|
||||
with InverterBase(reader, writer, 'tsun', Talent) as inverter:
|
||||
assert inverter.local.stream
|
||||
assert inverter.local.ifc
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert not Config.act_config['tsun']['enabled']
|
||||
assert inverter.remote.stream
|
||||
assert inverter.remote.ifc
|
||||
assert inverter.local.ifc.healthy()
|
||||
|
||||
# outside context manager the unhealth AsyncStream is released
|
||||
FakeWriter.peer = ("47.1.2.3", 10000)
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
assert inv.healthy() # inverter is healthy again (without the unhealty AsyncStream)
|
||||
cnt += 1
|
||||
del inv
|
||||
assert cnt == 1
|
||||
|
||||
del inverter
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remote_conn_to_loopback(config_conn, patch_open_connection):
|
||||
'''check DNS resolving of the TSUN FQDN to the loopback address'''
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
assert asyncio.get_running_loop()
|
||||
InverterBase._registry.clear()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
FakeWriter.peer = ("127.0.0.1", 10000)
|
||||
|
||||
with InverterBase(reader, writer, 'tsun', Talent) as inverter:
|
||||
assert inverter.local.stream
|
||||
assert inverter.local.ifc
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert not Config.act_config['tsun']['enabled']
|
||||
assert inverter.remote.stream
|
||||
assert inverter.remote.ifc
|
||||
assert inverter.local.ifc.healthy()
|
||||
|
||||
# outside context manager the unhealth AsyncStream is released
|
||||
FakeWriter.peer = ("47.1.2.3", 10000)
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
assert inv.healthy() # inverter is healthy again (without the unhealty AsyncStream)
|
||||
cnt += 1
|
||||
del inv
|
||||
assert cnt == 1
|
||||
|
||||
del inverter
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remote_conn_to_None(config_conn, patch_open_connection):
|
||||
'''check if get_extra_info() return None in case of an error'''
|
||||
_ = config_conn
|
||||
_ = patch_open_connection
|
||||
assert asyncio.get_running_loop()
|
||||
InverterBase._registry.clear()
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
FakeWriter.peer = None
|
||||
|
||||
with InverterBase(reader, writer, 'tsun', Talent) as inverter:
|
||||
assert inverter.local.stream
|
||||
assert inverter.local.ifc
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert Config.act_config['tsun']['enabled']
|
||||
assert inverter.remote.stream
|
||||
assert inverter.remote.ifc
|
||||
assert inverter.local.ifc.healthy()
|
||||
|
||||
# outside context manager the unhealth AsyncStream is released
|
||||
FakeWriter.peer = ("47.1.2.3", 10000)
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
assert inv.healthy() # inverter is healthy again (without the unhealty AsyncStream)
|
||||
cnt += 1
|
||||
del inv
|
||||
assert cnt == 1
|
||||
|
||||
del inverter
|
||||
cnt = 0
|
||||
for inv in InverterBase:
|
||||
print(f'InverterBase refs:{gc.get_referrers(inv)}')
|
||||
cnt += 1
|
||||
assert cnt == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unhealthy_remote(config_conn, patch_open_connection, patch_unhealthy_remote):
|
||||
_ = config_conn
|
||||
|
||||
@@ -5,15 +5,15 @@ import sys,gc
|
||||
|
||||
from mock import patch
|
||||
from enum import Enum
|
||||
from infos import Infos
|
||||
from cnf.config import Config
|
||||
from proxy import Proxy
|
||||
from inverter_base import InverterBase
|
||||
from singleton import Singleton
|
||||
from gen3.inverter_g3 import InverterG3
|
||||
from async_stream import AsyncStream
|
||||
from app.src.infos import Infos
|
||||
from app.src.config import Config
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.gen3.inverter_g3 import InverterG3
|
||||
from app.src.async_stream import AsyncStream
|
||||
|
||||
from test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
from app.tests.test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -59,7 +59,7 @@ class FakeWriter():
|
||||
return
|
||||
def get_extra_info(self, sel: str):
|
||||
if sel == 'peername':
|
||||
return ('47.1.2.3', 10000)
|
||||
return 'remote.intern'
|
||||
elif sel == 'sockname':
|
||||
return 'sock:1234'
|
||||
assert False
|
||||
@@ -70,13 +70,13 @@ class FakeWriter():
|
||||
async def wait_closed(self):
|
||||
return
|
||||
|
||||
class MockType(Enum):
|
||||
class TestType(Enum):
|
||||
RD_TEST_0_BYTES = 1
|
||||
RD_TEST_TIMEOUT = 2
|
||||
RD_TEST_EXCEPT = 3
|
||||
|
||||
|
||||
test = MockType.RD_TEST_0_BYTES
|
||||
test = TestType.RD_TEST_0_BYTES
|
||||
|
||||
@pytest.fixture
|
||||
def patch_open_connection():
|
||||
@@ -86,9 +86,9 @@ def patch_open_connection():
|
||||
|
||||
def new_open(host: str, port: int):
|
||||
global test
|
||||
if test == MockType.RD_TEST_TIMEOUT:
|
||||
if test == TestType.RD_TEST_TIMEOUT:
|
||||
raise ConnectionRefusedError
|
||||
elif test == MockType.RD_TEST_EXCEPT:
|
||||
elif test == TestType.RD_TEST_EXCEPT:
|
||||
raise ValueError("Value cannot be negative") # Compliant
|
||||
return new_conn(None)
|
||||
|
||||
@@ -144,14 +144,14 @@ async def test_remote_except(config_conn, patch_open_connection):
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
global test
|
||||
test = MockType.RD_TEST_TIMEOUT
|
||||
test = TestType.RD_TEST_TIMEOUT
|
||||
|
||||
with InverterG3(FakeReader(), FakeWriter()) as inverter:
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream==None
|
||||
|
||||
test = MockType.RD_TEST_EXCEPT
|
||||
test = TestType.RD_TEST_EXCEPT
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream==None
|
||||
|
||||
@@ -4,14 +4,14 @@ import asyncio
|
||||
|
||||
from mock import patch
|
||||
from enum import Enum
|
||||
from infos import Infos
|
||||
from cnf.config import Config
|
||||
from proxy import Proxy
|
||||
from inverter_base import InverterBase
|
||||
from singleton import Singleton
|
||||
from gen3plus.inverter_g3p import InverterG3P
|
||||
from app.src.infos import Infos
|
||||
from app.src.config import Config
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.gen3plus.inverter_g3p import InverterG3P
|
||||
|
||||
from test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
from app.tests.test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
@@ -58,7 +58,7 @@ class FakeWriter():
|
||||
return
|
||||
def get_extra_info(self, sel: str):
|
||||
if sel == 'peername':
|
||||
return ('47.1.2.3', 10000)
|
||||
return 'remote.intern'
|
||||
elif sel == 'sockname':
|
||||
return 'sock:1234'
|
||||
assert False
|
||||
@@ -69,13 +69,13 @@ class FakeWriter():
|
||||
async def wait_closed(self):
|
||||
return
|
||||
|
||||
class MockType(Enum):
|
||||
class TestType(Enum):
|
||||
RD_TEST_0_BYTES = 1
|
||||
RD_TEST_TIMEOUT = 2
|
||||
RD_TEST_EXCEPT = 3
|
||||
|
||||
|
||||
test = MockType.RD_TEST_0_BYTES
|
||||
test = TestType.RD_TEST_0_BYTES
|
||||
|
||||
@pytest.fixture
|
||||
def patch_open_connection():
|
||||
@@ -85,17 +85,16 @@ def patch_open_connection():
|
||||
|
||||
def new_open(host: str, port: int):
|
||||
global test
|
||||
if test == MockType.RD_TEST_TIMEOUT:
|
||||
if test == TestType.RD_TEST_TIMEOUT:
|
||||
raise ConnectionRefusedError
|
||||
elif test == MockType.RD_TEST_EXCEPT:
|
||||
elif test == TestType.RD_TEST_EXCEPT:
|
||||
raise ValueError("Value cannot be negative") # Compliant
|
||||
return new_conn(None)
|
||||
|
||||
with patch.object(asyncio, 'open_connection', new_open) as conn:
|
||||
yield conn
|
||||
|
||||
def test_method_calls(config_conn):
|
||||
_ = config_conn
|
||||
def test_method_calls():
|
||||
reader = FakeReader()
|
||||
writer = FakeWriter()
|
||||
InverterBase._registry.clear()
|
||||
@@ -122,14 +121,14 @@ async def test_remote_except(config_conn, patch_open_connection):
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
global test
|
||||
test = MockType.RD_TEST_TIMEOUT
|
||||
test = TestType.RD_TEST_TIMEOUT
|
||||
|
||||
with InverterG3P(FakeReader(), FakeWriter(), client_mode=False) as inverter:
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream==None
|
||||
|
||||
test = MockType.RD_TEST_EXCEPT
|
||||
test = TestType.RD_TEST_EXCEPT
|
||||
await inverter.create_remote()
|
||||
await asyncio.sleep(0)
|
||||
assert inverter.remote.stream==None
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import asyncio
|
||||
from modbus import Modbus
|
||||
from infos import Infos, Register
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.infos import Infos, Register
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ from aiomqtt import MqttCodeError
|
||||
|
||||
from mock import patch
|
||||
from enum import Enum
|
||||
from singleton import Singleton
|
||||
from cnf.config import Config
|
||||
from infos import Infos
|
||||
from mqtt import Mqtt
|
||||
from inverter_base import InverterBase
|
||||
from messages import Message, State
|
||||
from proxy import Proxy
|
||||
from modbus_tcp import ModbusConn, ModbusTcp
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.config import Config
|
||||
from app.src.infos import Infos
|
||||
from app.src.mqtt import Mqtt
|
||||
from app.src.inverter_base import InverterBase
|
||||
from app.src.messages import Message, State
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.modbus_tcp import ModbusConn, ModbusTcp
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -5,15 +5,13 @@ import aiomqtt
|
||||
import logging
|
||||
|
||||
from mock import patch, Mock
|
||||
from async_stream import AsyncIfcImpl
|
||||
from singleton import Singleton
|
||||
from mqtt import Mqtt
|
||||
from modbus import Modbus
|
||||
from gen3plus.solarman_v5 import SolarmanV5
|
||||
from cnf.config import Config
|
||||
from app.src.async_stream import AsyncIfcImpl
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.mqtt import Mqtt
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
||||
from app.src.config import Config
|
||||
|
||||
NO_MOSQUITTO_TEST = False
|
||||
'''disable all tests with connections to test.mosquitto.org'''
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -71,79 +69,23 @@ def spy_modbus_cmd_client():
|
||||
|
||||
def test_native_client(test_hostname, test_port):
|
||||
"""Sanity check: Make sure the paho-mqtt client can connect to the test
|
||||
MQTT server. Otherwise the test set NO_MOSQUITTO_TEST to True and disable
|
||||
all test cases which depends on the test.mosquitto.org server
|
||||
MQTT server.
|
||||
"""
|
||||
global NO_MOSQUITTO_TEST
|
||||
if NO_MOSQUITTO_TEST:
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import threading
|
||||
|
||||
c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
|
||||
c = mqtt.Client()
|
||||
c.loop_start()
|
||||
try:
|
||||
# Just make sure the client connects successfully
|
||||
on_connect = threading.Event()
|
||||
c.on_connect = Mock(side_effect=lambda *_: on_connect.set())
|
||||
c.connect_async(test_hostname, test_port)
|
||||
if not on_connect.wait(3):
|
||||
NO_MOSQUITTO_TEST = True # skip all mosquitto tests
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
assert on_connect.wait(5)
|
||||
finally:
|
||||
c.loop_stop()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_connection(config_mqtt_conn):
|
||||
global NO_MOSQUITTO_TEST
|
||||
if NO_MOSQUITTO_TEST:
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
|
||||
_ = config_mqtt_conn
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
assert m.task
|
||||
assert await asyncio.wait_for(on_connect.wait(), 5)
|
||||
# await asyncio.sleep(1)
|
||||
assert 0 == m.ha_restarts
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
except TimeoutError:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ha_reconnect(config_mqtt_conn):
|
||||
global NO_MOSQUITTO_TEST
|
||||
if NO_MOSQUITTO_TEST:
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
|
||||
_ = config_mqtt_conn
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'offline', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
assert not on_connect.is_set()
|
||||
|
||||
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'online', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
assert on_connect.is_set()
|
||||
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_no_config(config_no_conn):
|
||||
_ = config_no_conn
|
||||
@@ -168,6 +110,29 @@ async def test_mqtt_no_config(config_no_conn):
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_connection(config_mqtt_conn):
|
||||
_ = config_mqtt_conn
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
assert m.task
|
||||
assert await asyncio.wait_for(on_connect.wait(), 5)
|
||||
# await asyncio.sleep(1)
|
||||
assert 0 == m.ha_restarts
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
except TimeoutError:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_msg_dispatch(config_mqtt_conn, spy_modbus_cmd):
|
||||
_ = config_mqtt_conn
|
||||
@@ -244,6 +209,26 @@ async def test_msg_ignore_client_conn(config_mqtt_conn, spy_modbus_cmd_client):
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ha_reconnect(config_mqtt_conn):
|
||||
_ = config_mqtt_conn
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'offline', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
assert not on_connect.is_set()
|
||||
|
||||
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'online', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
assert on_connect.is_set()
|
||||
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ignore_unknown_func(config_mqtt_conn):
|
||||
'''don't dispatch for unknwon function names'''
|
||||
|
||||
@@ -5,11 +5,11 @@ import aiomqtt
|
||||
import logging
|
||||
|
||||
from mock import patch, Mock
|
||||
from singleton import Singleton
|
||||
from proxy import Proxy
|
||||
from mqtt import Mqtt
|
||||
from gen3plus.solarman_v5 import SolarmanV5
|
||||
from cnf.config import Config
|
||||
from app.src.singleton import Singleton
|
||||
from app.src.proxy import Proxy
|
||||
from app.src.mqtt import Mqtt
|
||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
||||
from app.src.config import Config
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import logging
|
||||
import os
|
||||
from mock import patch
|
||||
from server import get_log_level
|
||||
|
||||
def test_get_log_level():
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': ''}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.INFO
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': 'DEBUG'}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.DEBUG
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': 'WARN'}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.WARNING
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': 'UNKNOWN'}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.INFO
|
||||
@@ -1,16 +1,16 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
from singleton import Singleton
|
||||
from app.src.singleton import Singleton
|
||||
|
||||
class Example(metaclass=Singleton):
|
||||
class Test(metaclass=Singleton):
|
||||
def __init__(self):
|
||||
pass # is a dummy test class
|
||||
|
||||
def test_singleton_metaclass():
|
||||
Singleton._instances.clear()
|
||||
a = Example()
|
||||
a = Test()
|
||||
assert 1 == len(Singleton._instances)
|
||||
b = Example()
|
||||
b = Test()
|
||||
assert 1 == len(Singleton._instances)
|
||||
assert a is b
|
||||
del a
|
||||
|
||||
@@ -5,12 +5,12 @@ import asyncio
|
||||
import logging
|
||||
import random
|
||||
from math import isclose
|
||||
from async_stream import AsyncIfcImpl, StreamPtr
|
||||
from gen3plus.solarman_v5 import SolarmanV5, SolarmanBase
|
||||
from cnf.config import Config
|
||||
from infos import Infos, Register
|
||||
from modbus import Modbus
|
||||
from messages import State, Message
|
||||
from app.src.async_stream import AsyncIfcImpl, StreamPtr
|
||||
from app.src.gen3plus.solarman_v5 import SolarmanV5, SolarmanBase
|
||||
from app.src.config import Config
|
||||
from app.src.infos import Infos, Register
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.messages import State, Message
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
@@ -1730,6 +1730,7 @@ async def test_modbus_polling(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp
|
||||
assert asyncio.get_running_loop() == m.mb_timer.loop
|
||||
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||
assert m.mb_timer.tim == None
|
||||
|
||||
m.read() # read complete msg, and dispatch msg
|
||||
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||
assert m.msg_count == 1
|
||||
@@ -1767,6 +1768,10 @@ async def test_start_client_mode(config_tsun_inv1, str_test_ip):
|
||||
_ = config_tsun_inv1
|
||||
assert asyncio.get_running_loop()
|
||||
m = MemoryStream(b'')
|
||||
m.mb_start_reg = 0x3000
|
||||
m.mb_incr_reg = 0x00 # 4
|
||||
m.mb_scan_len = 48
|
||||
|
||||
assert m.state == State.init
|
||||
assert m.no_forwarding == False
|
||||
assert m.mb_timer.tim == None
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
|
||||
from async_stream import AsyncIfcImpl, StreamPtr
|
||||
from gen3plus.solarman_v5 import SolarmanV5, SolarmanBase
|
||||
from gen3plus.solarman_emu import SolarmanEmu
|
||||
from infos import Infos, Register
|
||||
|
||||
from test_solarman import FakeIfc, MemoryStream, get_sn_int, get_sn, correct_checksum, config_tsun_inv1, msg_modbus_rsp
|
||||
from test_infos_g3p import str_test_ip, bytes_test_ip
|
||||
from app.src.async_stream import AsyncIfcImpl, StreamPtr
|
||||
from app.src.gen3plus.solarman_v5 import SolarmanV5, SolarmanBase
|
||||
from app.src.gen3plus.solarman_emu import SolarmanEmu
|
||||
from app.src.infos import Infos, Register
|
||||
from app.tests.test_solarman import FakeIfc, MemoryStream, get_sn_int, get_sn, correct_checksum, config_tsun_inv1, msg_modbus_rsp
|
||||
from app.tests.test_infos_g3p import str_test_ip, bytes_test_ip
|
||||
|
||||
timestamp = 0x3224c8bc
|
||||
|
||||
@@ -172,7 +170,6 @@ async def test_snd_inv_data(config_tsun_inv1, inverter_ind_msg, inverter_rsp_msg
|
||||
inv.db.set_db_def_value(Register.GRID_VOLTAGE, 224.8)
|
||||
inv.db.set_db_def_value(Register.GRID_CURRENT, 0.73)
|
||||
inv.db.set_db_def_value(Register.GRID_FREQUENCY, 50.05)
|
||||
inv.db.set_db_def_value(Register.PROD_COMPL_TYPE, 6)
|
||||
assert asyncio.get_running_loop() == inv.mb_timer.loop
|
||||
await inv.send_start_cmd(get_sn_int(), str_test_ip, False, inv.mb_first_timeout)
|
||||
inv.db.set_db_def_value(Register.DATA_UP_INTERVAL, 17) # set test value
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# test_with_pytest.py
|
||||
import pytest, logging, asyncio
|
||||
from math import isclose
|
||||
from async_stream import AsyncIfcImpl, StreamPtr
|
||||
from gen3.talent import Talent, Control
|
||||
from cnf.config import Config
|
||||
from infos import Infos, Register
|
||||
from modbus import Modbus
|
||||
from messages import State
|
||||
from app.src.async_stream import AsyncIfcImpl, StreamPtr
|
||||
from app.src.gen3.talent import Talent, Control
|
||||
from app.src.config import Config
|
||||
from app.src.infos import Infos, Register
|
||||
from app.src.modbus import Modbus
|
||||
from app.src.messages import State
|
||||
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
@@ -328,90 +328,6 @@ def msg_inverter_ind_new(): # Data indication from DSP V5.0.17
|
||||
msg += b'\x00\x00\x00\x00'
|
||||
return msg
|
||||
|
||||
@pytest.fixture
|
||||
def msg_inverter_ind_new2(): # Data indication from DSP V5.0.17
|
||||
msg = b'\x00\x00\x04\xf4\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001'
|
||||
msg += b'\x01\x00\x00\x01'
|
||||
msg += b'\x86\x98\x55\xe7\x48\x00\x00\x00\xa3\x00\x00\x01\x93\x53\x00\x00'
|
||||
msg += b'\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00'
|
||||
msg += b'\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00\x00\x00\x00\x01\x98'
|
||||
msg += b'\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00'
|
||||
msg += b'\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00'
|
||||
msg += b'\x00\x01\x9d\x53\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01'
|
||||
msg += b'\x9f\x53\x00\x00\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf6\x53'
|
||||
msg += b'\x00\x00\x00\x00\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00'
|
||||
msg += b'\x00\x00\x01\xf9\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00'
|
||||
msg += b'\x01\xfb\x53\x00\x00\x00\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd'
|
||||
msg += b'\x53\x00\x00\x00\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00'
|
||||
msg += b'\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00\x00'
|
||||
msg += b'\x00\x02\x02\x53\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02'
|
||||
msg += b'\x04\x53\x00\x00\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02'
|
||||
msg += b'\x59\x53\x00\x00\x00\x00\x02\x5a\x53\x00\x00\x00\x00\x02\x5b\x53'
|
||||
msg += b'\x00\x00\x00\x00\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00'
|
||||
msg += b'\x00\x00\x02\x5e\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00'
|
||||
msg += b'\x02\x60\x53\x00\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62'
|
||||
msg += b'\x53\x00\x00\x00\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00'
|
||||
msg += b'\x00\x00\x00\x02\x65\x53\x00\x00\x00\x00\x02\x66\x53\x00\x00\x00'
|
||||
msg += b'\x00\x02\x67\x53\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02'
|
||||
msg += b'\xbc\x49\x00\x00\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02'
|
||||
msg += b'\xbe\x53\x00\x00\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53'
|
||||
msg += b'\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00'
|
||||
msg += b'\x00\x00\x02\xc3\x53\x00\x00\x00\x00\x02\xc4\x53\x00\x00\x00\x00'
|
||||
msg += b'\x02\xc5\x53\x00\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7'
|
||||
msg += b'\x53\x00\x00\x00\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00'
|
||||
msg += b'\x00\x00\x00\x02\xca\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00'
|
||||
msg += b'\x00\x02\xcc\x53\x00\x00\x00\x00\x03\x20\x53\x00\x00\x00\x00\x03'
|
||||
msg += b'\x84\x53\x50\x11\x00\x00\x03\xe8\x46\x43\x65\xcc\xcd\x00\x00\x04'
|
||||
msg += b'\x4c\x46\x40\x0c\xcc\xcd\x00\x00\x04\xb0\x46\x42\x47\xd7\x0a\x00'
|
||||
msg += b'\x00\x05\x14\x53\x00\x35\x00\x00\x05\x78\x53\x00\x00\x00\x00\x05'
|
||||
msg += b'\xdc\x53\x03\x20\x00\x00\x06\x40\x46\x43\xfd\x4c\xcd\x00\x00\x06'
|
||||
msg += b'\xa4\x46\x42\x18\x00\x00\x00\x00\x07\x08\x46\x40\xde\x14\x7b\x00'
|
||||
msg += b'\x00\x07\x6c\x46\x43\x84\x33\x33\x00\x00\x07\xd0\x46\x42\x1a\x00'
|
||||
msg += b'\x00\x00\x00\x08\x34\x46\x40\xda\x8f\x5c\x00\x00\x08\x98\x46\x43'
|
||||
msg += b'\x83\xb3\x33\x00\x00\x08\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60'
|
||||
msg += b'\x46\x00\x00\x00\x00\x00\x00\x09\xc4\x46\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x0a\x28\x46\x00\x00\x00\x00\x00\x00\x0a\x8c\x46\x00\x00\x00\x00'
|
||||
msg += b'\x00\x00\x0a\xf0\x46\x00\x00\x00\x00\x00\x00\x0b\x54\x46\x40\x9c'
|
||||
msg += b'\xcc\xcd\x00\x00\x0b\xb8\x46\x43\xea\xb5\xc3\x00\x00\x0c\x1c\x46'
|
||||
msg += b'\x40\x1e\xb8\x52\x00\x00\x0c\x80\x46\x43\x6d\x2b\x85\x00\x00\x0c'
|
||||
msg += b'\xe4\x46\x40\x1a\xe1\x48\x00\x00\x0d\x48\x46\x43\x68\x40\x00\x00'
|
||||
msg += b'\x00\x0d\xac\x46\x00\x00\x00\x00\x00\x00\x0e\x10\x46\x00\x00\x00'
|
||||
msg += b'\x00\x00\x00\x0e\x74\x46\x00\x00\x00\x00\x00\x00\x0e\xd8\x46\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0\x53\x00'
|
||||
msg += b'\x00\x00\x00\x10\x04\x53\x55\xaa\x00\x00\x10\x68\x53\x00\x01\x00'
|
||||
msg += b'\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00\x00\x00\x11'
|
||||
msg += b'\x94\x53\x00\x00\x00\x00\x11\xf8\x53\xff\xff\x00\x00\x12\x5c\x53'
|
||||
msg += b'\xff\xff\x00\x00\x12\xc0\x53\x00\x00\x00\x00\x13\x24\x53\xff\xff'
|
||||
msg += b'\x00\x00\x13\x88\x53\xff\xff\x00\x00\x13\xec\x53\xff\xff\x00\x00'
|
||||
msg += b'\x14\x50\x53\xff\xff\x00\x00\x14\xb4\x53\xff\xff\x00\x00\x15\x18'
|
||||
msg += b'\x53\xff\xff\x00\x00\x15\x7c\x53\x00\x00\x00\x00\x27\x10\x53\x00'
|
||||
msg += b'\x02\x00\x00\x27\x74\x53\x00\x3c\x00\x00\x27\xd8\x53\x00\x68\x00'
|
||||
msg += b'\x00\x28\x3c\x53\x05\x00\x00\x00\x28\xa0\x46\x43\x79\x00\x00\x00'
|
||||
msg += b'\x00\x29\x04\x46\x43\x48\x00\x00\x00\x00\x29\x68\x46\x42\x48\x33'
|
||||
msg += b'\x33\x00\x00\x29\xcc\x46\x42\x3e\x3d\x71\x00\x00\x2a\x30\x53\x00'
|
||||
msg += b'\x01\x00\x00\x2a\x94\x46\x43\x37\x00\x00\x00\x00\x2a\xf8\x46\x42'
|
||||
msg += b'\xce\x00\x00\x00\x00\x2b\x5c\x53\x00\x96\x00\x00\x2b\xc0\x53\x00'
|
||||
msg += b'\x10\x00\x00\x2c\x24\x46\x43\x90\x00\x00\x00\x00\x2c\x88\x46\x43'
|
||||
msg += b'\x95\x00\x00\x00\x00\x2c\xec\x53\x00\x06\x00\x00\x2d\x50\x53\x00'
|
||||
msg += b'\x06\x00\x00\x2d\xb4\x46\x43\x7d\x00\x00\x00\x00\x2e\x18\x46\x42'
|
||||
msg += b'\x3d\xeb\x85\x00\x00\x2e\x7c\x46\x42\x3d\xeb\x85\x00\x00\x2e\xe0'
|
||||
msg += b'\x53\x00\x03\x00\x00\x2f\x44\x53\x00\x03\x00\x00\x2f\xa8\x46\x42'
|
||||
msg += b'\x4d\xeb\x85\x00\x00\x30\x0c\x46\x42\x4d\xeb\x85\x00\x00\x30\x70'
|
||||
msg += b'\x53\x00\x03\x00\x00\x30\xd4\x53\x00\x03\x00\x00\x31\x38\x46\x42'
|
||||
msg += b'\x08\x00\x00\x00\x00\x31\x9c\x53\x00\x05\x00\x00\x32\x00\x53\x04'
|
||||
msg += b'\x00\x00\x00\x32\x64\x53\x00\x01\x00\x00\x32\xc8\x53\x13\x9c\x00'
|
||||
msg += b'\x00\x33\x2c\x53\x0f\xa0\x00\x00\x33\x90\x53\x00\x4f\x00\x00\x33'
|
||||
msg += b'\xf4\x53\x00\x66\x00\x00\x34\x58\x53\x03\xe8\x00\x00\x34\xbc\x53'
|
||||
msg += b'\x04\x00\x00\x00\x35\x20\x53\x00\x00\x00\x00\x35\x84\x53\x00\x00'
|
||||
msg += b'\x00\x00\x35\xe8\x53\x00\x00\x00\x00\x36\x4c\x53\x00\x00\x00\x01'
|
||||
msg += b'\x38\x80\x53\x00\x02\x00\x01\x38\x81\x53\x00\x01\x00\x01\x38\x82'
|
||||
msg += b'\x53\x00\x01\x00\x01\x38\x83\x53\x00\x00\x00\x00\x00\x0a\x08\x00'
|
||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x14\x04\x00\x00\x00\x00\x00\x00\x00'
|
||||
msg += b'\x00\x1e\x07\x00\x00\x00\x00\x00'
|
||||
return msg
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def msg_inverter_ind_0w(): # Data indication with 0.5W grid output
|
||||
msg = b'\x00\x00\x05\x02\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001'
|
||||
@@ -2235,34 +2151,3 @@ def test_timeout(config_tsun_inv1):
|
||||
m.modbus_polling = False
|
||||
assert Talent.MAX_DEF_IDLE_TIME == m._timeout()
|
||||
m.close()
|
||||
|
||||
def test_msg_inv_replay(config_tsun_inv1, msg_inverter_ind_0w, msg_inverter_ind_new2):
|
||||
'''replay must be ignores, since HA only supports realtime values'''
|
||||
_ = config_tsun_inv1
|
||||
|
||||
m = MemoryStream(msg_inverter_ind_0w, (0,)) # realtime msg with 0.5W Output Power
|
||||
m.append_msg(msg_inverter_ind_new2) # replay msg with 506.6W Output Power
|
||||
m.db.db['grid'] = {'Output_Power': 100}
|
||||
m.db.stat['proxy']['Unknown_Ctrl'] = 0
|
||||
m.db.stat['proxy']['Invalid_Data_Type'] = 0
|
||||
m.read() # read complete msg, and dispatch msg
|
||||
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
|
||||
assert m.db.stat['proxy']['Invalid_Data_Type'] == 0
|
||||
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
|
||||
assert m.msg_count == 2
|
||||
assert m.msg_recvd[0]['ctrl']==145
|
||||
assert m.msg_recvd[0]['msg_id']==4
|
||||
assert m.msg_recvd[0]['header_len']==23
|
||||
assert m.msg_recvd[0]['data_len']==1263
|
||||
assert m.msg_recvd[1]['ctrl']==145
|
||||
assert m.msg_recvd[1]['msg_id']==4
|
||||
assert m.msg_recvd[1]['header_len']==23
|
||||
assert m.msg_recvd[1]['data_len']==1249
|
||||
assert m.id_str == b"R170000000000001"
|
||||
assert m.unique_id == 'R170000000000001'
|
||||
|
||||
assert m.db.get_db_value(Register.INVERTER_STATUS) == 1
|
||||
assert isclose(m.db.db['grid']['Output_Power'], 0.5) # must be 0.5W not 100W nor 506.6W
|
||||
|
||||
m.close()
|
||||
assert m.db.get_db_value(Register.INVERTER_STATUS) == 0
|
||||
|
||||
2
ha_addons/.gitignore
vendored
2
ha_addons/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
.data.json
|
||||
config.yaml
|
||||
@@ -1,136 +0,0 @@
|
||||
#!make
|
||||
include ../.env
|
||||
|
||||
.PHONY: debug dev build clean rootfs repro rc rel
|
||||
|
||||
SHELL = /bin/sh
|
||||
JINJA = jinja2
|
||||
IMAGE = tsun-gen3-addon
|
||||
|
||||
|
||||
# Folders
|
||||
SRC=../app
|
||||
SRC_PROXY=$(SRC)/src
|
||||
CNF_PROXY=$(SRC)/config
|
||||
|
||||
ADDON_PATH = ha_addon
|
||||
DST=$(ADDON_PATH)/rootfs
|
||||
DST_PROXY=$(DST)/home/proxy
|
||||
|
||||
INST_BASE=../../ha-addons/ha-addons
|
||||
|
||||
TEMPL=templates
|
||||
|
||||
# collect source files
|
||||
SRC_FILES := $(wildcard $(SRC_PROXY)/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/*.ini)\
|
||||
$(wildcard $(SRC_PROXY)/cnf/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/gen3/*.py)\
|
||||
$(wildcard $(SRC_PROXY)/gen3plus/*.py)
|
||||
CNF_FILES := $(wildcard $(CNF_PROXY)/*.toml)
|
||||
|
||||
# determine destination files
|
||||
TARGET_FILES = $(SRC_FILES:$(SRC_PROXY)/%=$(DST_PROXY)/%)
|
||||
CONFIG_FILES = $(CNF_FILES:$(CNF_PROXY)/%=$(DST_PROXY)/%)
|
||||
|
||||
export BUILD_DATE := ${shell date -Iminutes}
|
||||
VERSION := $(shell cat $(SRC)/.version)
|
||||
export MAJOR := $(shell echo $(VERSION) | cut -f1 -d.)
|
||||
|
||||
PUBLIC_URL := $(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f1 -d/)
|
||||
PUBLIC_USER :=$(shell echo $(PUBLIC_CONTAINER_REGISTRY) | cut -f2 -d/)
|
||||
|
||||
|
||||
dev debug: build
|
||||
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PRIVAT_CONTAINER_REGISTRY)$(IMAGE)
|
||||
export VERSION=$(VERSION)-$@ && \
|
||||
export IMAGE=$(PRIVAT_CONTAINER_REGISTRY)$(IMAGE) && \
|
||||
docker buildx bake -f docker-bake.hcl $@
|
||||
|
||||
rc rel: build
|
||||
@echo version: $(VERSION) build-date: $(BUILD_DATE) image: $(PUBLIC_CONTAINER_REGISTRY)$(IMAGE)
|
||||
@echo login at $(PUBLIC_URL) as $(PUBLIC_USER)
|
||||
@DO_LOGIN="$(shell echo $(PUBLIC_CR_KEY) | docker login $(PUBLIC_URL) -u $(PUBLIC_USER) --password-stdin)"
|
||||
export VERSION=$(VERSION)-$@ && \
|
||||
export IMAGE=$(PUBLIC_CONTAINER_REGISTRY)$(IMAGE) && \
|
||||
docker buildx bake -f docker-bake.hcl $@
|
||||
|
||||
|
||||
build: rootfs $(ADDON_PATH)/config.yaml repro
|
||||
|
||||
clean:
|
||||
rm -r -f $(DST_PROXY)
|
||||
rm -f $(DST)/requirements.txt
|
||||
rm -f $(ADDON_PATH)/config.yaml
|
||||
rm -f $(TEMPL)/.data.json
|
||||
|
||||
#
|
||||
# Build rootfs and config.yaml as local add-on
|
||||
# The rootfs is needed to build the add-on Dockercontainers
|
||||
#
|
||||
|
||||
rootfs: $(TARGET_FILES) $(CONFIG_FILES) $(DST)/requirements.txt
|
||||
|
||||
STAGE=dev
|
||||
debug : STAGE=debug
|
||||
rc : STAGE=rc
|
||||
rel : STAGE=rel
|
||||
|
||||
$(CONFIG_FILES): $(DST_PROXY)/% : $(CNF_PROXY)/%
|
||||
@echo Copy $< to $@
|
||||
@mkdir -p $(@D)
|
||||
@cp $< $@
|
||||
|
||||
$(TARGET_FILES): $(DST_PROXY)/% : $(SRC_PROXY)/%
|
||||
@echo Copy $< to $@
|
||||
@mkdir -p $(@D)
|
||||
@cp $< $@
|
||||
|
||||
$(DST)/requirements.txt : $(SRC)/requirements.txt
|
||||
@echo Copy $< to $@
|
||||
@cp $< $@
|
||||
|
||||
$(ADDON_PATH)/%.yaml: $(TEMPL)/%.jinja $(TEMPL)/.data.json
|
||||
$(JINJA) --strict -D AppVersion=$(VERSION) --format=json $^ -o $@
|
||||
|
||||
$(TEMPL)/.data.json: FORCE
|
||||
rsync --checksum $(TEMPL)/$(STAGE)_data.json $@
|
||||
|
||||
FORCE : ;
|
||||
|
||||
|
||||
#
|
||||
# Build repository for Home Assistant Add-On
|
||||
#
|
||||
|
||||
INST=$(INST_BASE)/ha_addon_dev
|
||||
repro_files = DOCS.md icon.png logo.png translations/de.yaml translations/en.yaml
|
||||
repro_root = CHANGELOG.md
|
||||
repro_templates = config.yaml
|
||||
repro_subdirs = translations
|
||||
repro_vers = debug dev rel
|
||||
|
||||
repro_all_files := $(foreach dir,$(repro_vers), $(foreach file,$(repro_files),$(INST_BASE)/ha_addon_$(dir)/$(file)))
|
||||
repro_root_files := $(foreach dir,$(repro_vers), $(foreach file,$(repro_root),$(INST_BASE)/ha_addon_$(dir)/$(file)))
|
||||
repro_all_templates := $(foreach dir,$(repro_vers), $(foreach file,$(repro_templates),$(INST_BASE)/ha_addon_$(dir)/$(file)))
|
||||
repro_all_subdirs := $(foreach dir,$(repro_vers), $(foreach file,$(repro_subdirs),$(INST_BASE)/ha_addon_$(dir)/$(file)))
|
||||
|
||||
repro: $(repro_all_subdirs) $(repro_all_templates) $(repro_all_files) $(repro_root_files)
|
||||
|
||||
$(repro_all_subdirs) :
|
||||
mkdir -p $@
|
||||
|
||||
$(repro_all_templates) : $(INST_BASE)/ha_addon_%/config.yaml: $(TEMPL)/config.jinja $(TEMPL)/%_data.json $(SRC)/.version
|
||||
$(JINJA) --strict -D AppVersion=$(VERSION)-$* $< $(filter %.json,$^) -o $@
|
||||
|
||||
$(repro_root_files) : %/CHANGELOG.md : ../CHANGELOG.md
|
||||
cp $< $@
|
||||
|
||||
$(filter $(INST_BASE)/ha_addon_debug/%,$(repro_all_files)) : $(INST_BASE)/ha_addon_debug/% : ha_addon/%
|
||||
cp $< $@
|
||||
$(filter $(INST_BASE)/ha_addon_dev/%,$(repro_all_files)) : $(INST_BASE)/ha_addon_dev/% : ha_addon/%
|
||||
cp $< $@
|
||||
$(filter $(INST_BASE)/ha_addon_rel/%,$(repro_all_files)) : $(INST_BASE)/ha_addon_rel/% : ha_addon/%
|
||||
cp $< $@
|
||||
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
variable "IMAGE" {
|
||||
default = "tsun-gen3-addon"
|
||||
}
|
||||
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 = "ha_addon"
|
||||
dockerfile = "Dockerfile"
|
||||
args = {
|
||||
VERSION = "${VERSION}"
|
||||
environment = "production"
|
||||
}
|
||||
attest = [
|
||||
"type =provenance,mode=max",
|
||||
"type =sbom,generator=docker/scout-sbom-indexer:latest"
|
||||
]
|
||||
annotations = [
|
||||
"index:io.hass.version=${VERSION}",
|
||||
"index:io.hass.type=addon",
|
||||
"index:io.hass.arch=armhf|aarch64|i386|amd64",
|
||||
"index:org.opencontainers.image.title=TSUN-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/ha_addons/ha_addon"
|
||||
]
|
||||
labels = {
|
||||
"io.hass.version" = "${VERSION}"
|
||||
"io.hass.type" = "addon"
|
||||
"io.hass.arch" = "armhf|aarch64|i386|amd64"
|
||||
"org.opencontainers.image.title" = "TSUN-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/ha_addonsha_addon"
|
||||
}
|
||||
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}:preview", "${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
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
# Home Assistant Add-on: TSUN Proxy
|
||||
|
||||
[TSUN Proxy][tsunproxy] 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 Home Assistant.
|
||||
This works even without an internet connection.
|
||||
The optional connection to the TSUN Cloud can be disabled!
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
1. This Add-on requires an MQTT broker to work.
|
||||
For a typical installation, we recommend the [Mosquitto add-on][Mosquitto] running on your Home Assistant.
|
||||
|
||||
2. You need to loop the proxy into the connection between the inverter and the TSUN Cloud,
|
||||
you must adapt the DNS record within the network that your inverter uses. You need a mapping
|
||||
from logger.talent-monitoring.com and/or iot.talent-monitoring.com to the IP address of your
|
||||
Home Assistant.
|
||||
This can be done, for example, by adding a local DNS record to [AdGuard Home Add-on][AdGuard]
|
||||
(navigate to `filters` on the AdGuard panel and add an entry under `custom filtering rules`).
|
||||
|
||||
## Installation
|
||||
|
||||
The installation of this add-on is pretty straightforward and not different in
|
||||
comparison to installing any other Home Assistant add-on.
|
||||
|
||||
1. Add the repository URL to the Home Assistant add-on store
|
||||
[![Add repository on my Home Assistant][repository-badge]][repository-url]
|
||||
2. Reload the add-on store page
|
||||
3. Click the "Install" button to install the add-on.
|
||||
4. Add your inverter configuration to the add-on configuration
|
||||
5. Start the "TSUN-Proxy" add-on
|
||||
6. Check the logs of the "TSUN-Proxy" add-on to see if everything went well.
|
||||
|
||||
_Please note, the add-on is pre-configured to connect with
|
||||
Home Assistants default MQTT Broker. There is no need to configure any MQTT parameters
|
||||
if you're running an MOSQUITTO add-on. Home Assistant communication and TSUN Cloud URL
|
||||
and Ports are also pre-configured._
|
||||
|
||||
This automatic handling of the TSUN Cloud and MQTT Broker conflicts with the
|
||||
[TSUN Proxy official documentation][tsunproxy]. The official documentation
|
||||
will state `mqtt.host`, `mqtt.port`, `mqtt.user`, `mqtt.passwd` `solarman.host`,
|
||||
`solarman.port` `tsun.host`, `tsun.port` and Home Assistant options are required.
|
||||
For the add-on, however, this isn't needed.
|
||||
|
||||
## Configuration
|
||||
|
||||
**Note**: _Remember to restart the add-on when the configuration is changed._
|
||||
|
||||
Example add-on configuration after installation:
|
||||
|
||||
```yaml
|
||||
inverters:
|
||||
- serial: R17E760702080400
|
||||
node_id: PV-Garage
|
||||
suggested_area: Garage
|
||||
modbus_polling: false
|
||||
pv1.manufacturer: Shinefar
|
||||
pv1.type: SF-M18/144550
|
||||
pv2.manufacturer: Shinefar
|
||||
pv2.type: SF-M18/144550
|
||||
```
|
||||
|
||||
**Note**: _This is just an example, you need to replace the values with your own!_
|
||||
|
||||
Example add-on configuration for GEN3PLUS inverters:
|
||||
|
||||
```yaml
|
||||
inverters:
|
||||
- serial: Y17000000000000
|
||||
monitor_sn: '2000000000'
|
||||
node_id: PV-Garage
|
||||
suggested_area: Garage
|
||||
modbus_polling: true
|
||||
client_mode.host: 192.168.x.x
|
||||
client_mode.port: 8899
|
||||
client_mode.forward: true
|
||||
pv1.manufacturer: Shinefar
|
||||
pv1.type: SF-M18/144550
|
||||
pv2.manufacturer: Shinefar
|
||||
pv2.type: SF-M18/144550
|
||||
pv3.manufacturer: Shinefar
|
||||
pv3.type: SF-M18/144550
|
||||
pv4.manufacturer: Shinefar
|
||||
pv4.type: SF-M18/144550
|
||||
```
|
||||
|
||||
**Note**: _This is just an example, you need to replace the values with your own!_
|
||||
|
||||
more information about the configuration can be found in the [configuration details page][configdetails].
|
||||
|
||||
## MQTT settings
|
||||
|
||||
By default, this add-on requires no `mqtt` config from the user. **This is not an error!**
|
||||
|
||||
However, you are free to set them if you want to override, however, in
|
||||
general usage, that should not be needed and is not recommended for this add-on.
|
||||
|
||||
## Changelog & Releases
|
||||
|
||||
This repository keeps a change log using [GitHub's releases][releases]
|
||||
functionality.
|
||||
|
||||
Releases are based on [Semantic Versioning][semver], and use the format
|
||||
of `MAJOR.MINOR.PATCH`. In a nutshell, the version will be incremented
|
||||
based on the following:
|
||||
|
||||
- `MAJOR`: Incompatible or major changes.
|
||||
- `MINOR`: Backwards-compatible new features and enhancements.
|
||||
- `PATCH`: Backwards-compatible bugfixes and package updates.
|
||||
|
||||
## Support
|
||||
|
||||
Got questions?
|
||||
|
||||
You have several options to get them answered:
|
||||
|
||||
- The Discussions section on [GitHub][discussions].
|
||||
- The [Home Assistant Discord chat server][discord-ha] for general Home
|
||||
Assistant discussions and questions.
|
||||
|
||||
You could also [open an issue here][issue] GitHub.
|
||||
|
||||
## Authors & contributors
|
||||
|
||||
The original setup of this repository is by [Stefan Allius][author].
|
||||
|
||||
We're very happy to receive contributions to this project! You can get started by reading [CONTRIBUTING.md][contribute].
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [BSD 3-clause License][bsd].
|
||||
|
||||
Note the aiomqtt library used is based on the paho-mqtt library, which has a dual license.
|
||||
One of the licenses is the so-called [Eclipse Distribution License v1.0.][eclipse]
|
||||
It is almost word-for-word identical to the BSD 3-clause License. The only differences are:
|
||||
|
||||
- One use of "COPYRIGHT OWNER" (EDL) instead of "COPYRIGHT HOLDER" (BSD)
|
||||
- One use of "Eclipse Foundation, Inc." (EDL) instead of "copyright holder" (BSD)
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
[tsunproxy]: https://github.com/s-allius/tsun-gen3-proxy
|
||||
[discussions]: https://github.com/s-allius/tsun-gen3-proxy/discussions
|
||||
[author]: https://github.com/s-allius
|
||||
[discord-ha]: https://discord.gg/c5DvZ4e
|
||||
[issue]: https://github.com/s-allius/tsun-gen3-proxy/issues
|
||||
[releases]: https://github.com/s-allius/tsun-gen3-proxy/releases
|
||||
[contribute]: https://github.com/s-allius/tsun-gen3-proxy/blob/main/CONTRIBUTING.md
|
||||
[semver]: http://semver.org/spec/v2.0.0.htm
|
||||
[bsd]: https://opensource.org/licenses/BSD-3-Clause
|
||||
[eclipse]: https://www.eclipse.org/org/documents/edl-v10.php
|
||||
[Mosquitto]: https://github.com/home-assistant/addons/blob/master/mosquitto/DOCS.md
|
||||
[AdGuard]: https://github.com/hassio-addons/addon-adguard-home
|
||||
[repository-badge]: https://img.shields.io/badge/Add%20repository%20to%20my-Home%20Assistant-41BDF5?logo=home-assistant&style=for-the-badge
|
||||
[repository-url]: https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Fs-allius%2Fha-addons
|
||||
[configdetails]: https://github.com/s-allius/tsun-gen3-proxy/wiki/Configuration-details
|
||||
@@ -1,90 +0,0 @@
|
||||
|
||||
############################################################################
|
||||
#
|
||||
# TSUN Proxy
|
||||
# Homeassistant Add-on
|
||||
#
|
||||
# based on https://github.com/s-allius/tsun-gen3-proxy/tree/main
|
||||
#
|
||||
############################################################################
|
||||
|
||||
|
||||
######################
|
||||
# 1 Build Base Image #
|
||||
######################
|
||||
|
||||
ARG BUILD_FROM="ghcr.io/hassio-addons/base:17.0.1"
|
||||
# hadolint ignore=DL3006
|
||||
FROM $BUILD_FROM AS base
|
||||
|
||||
# Installiere Python, pip und virtuelle Umgebungstools
|
||||
RUN apk add --no-cache python3=3.12.8-r1 py3-pip=24.3.1-r0 && \
|
||||
python -m venv /opt/venv && \
|
||||
. /opt/venv/bin/activate
|
||||
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
|
||||
|
||||
#######################
|
||||
# 2 Build wheel #
|
||||
#######################
|
||||
FROM base AS builder
|
||||
|
||||
COPY rootfs/requirements.txt /root/
|
||||
|
||||
RUN apk add --no-cache build-base=0.5-r3 && \
|
||||
python -m pip install --no-cache-dir wheel==0.45.1 && \
|
||||
python -OO -m pip wheel --no-cache-dir --wheel-dir=/root/wheels -r /root/requirements.txt
|
||||
|
||||
|
||||
|
||||
|
||||
#######################
|
||||
# 3 Build runtime #
|
||||
#######################
|
||||
FROM base AS runtime
|
||||
|
||||
ARG SERVICE_NAME
|
||||
ARG VERSION
|
||||
ENV SERVICE_NAME=${SERVICE_NAME}
|
||||
|
||||
|
||||
|
||||
#######################
|
||||
# 4 Install libraries #
|
||||
#######################
|
||||
|
||||
# install the requirements from the wheels packages from the builder stage
|
||||
# and unistall python packages and alpine package manger to reduce attack surface
|
||||
|
||||
COPY --from=builder /root/wheels /root/wheels
|
||||
RUN python -m pip install --no-cache-dir --no-cache --no-index /root/wheels/* && \
|
||||
rm -rf /root/wheels && \
|
||||
python -m pip uninstall --yes wheel pip && \
|
||||
apk --purge del apk-tools
|
||||
|
||||
|
||||
#######################
|
||||
# 5 copy data #
|
||||
#######################
|
||||
|
||||
COPY rootfs/ /
|
||||
|
||||
|
||||
|
||||
#######################
|
||||
# 6 run app #
|
||||
#######################
|
||||
|
||||
# make run.sh executable
|
||||
RUN chmod a+x /run.sh && \
|
||||
echo ${VERSION} > /proxy-version.txt
|
||||
|
||||
# command to run on container start
|
||||
CMD [ "/run.sh" ]
|
||||
|
||||
|
||||
|
||||
#######################
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 113 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 113 KiB |
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
|
||||
echo "Add-on environment started"
|
||||
|
||||
echo "check for Home Assistant MQTT"
|
||||
MQTT_HOST=$(bashio::services mqtt "host")
|
||||
MQTT_PORT=$(bashio::services mqtt "port")
|
||||
MQTT_USER=$(bashio::services mqtt "username")
|
||||
MQTT_PASSWORD=$(bashio::services mqtt "password")
|
||||
|
||||
# if a MQTT was/not found, drop a note
|
||||
if [ -z "$MQTT_HOST" ]; then
|
||||
echo "MQTT not found"
|
||||
else
|
||||
echo "MQTT found"
|
||||
export MQTT_HOST
|
||||
export MQTT_PORT
|
||||
export MQTT_USER
|
||||
export MQTT_PASSWORD
|
||||
fi
|
||||
|
||||
|
||||
|
||||
|
||||
# Create folder for log und config files
|
||||
mkdir -p /homeassistant/tsun-proxy/logs
|
||||
|
||||
cd /home/proxy || exit
|
||||
|
||||
export VERSION=$(cat /proxy-version.txt)
|
||||
|
||||
echo "Start Proxyserver..."
|
||||
python3 server.py --json_config=/data/options.json --log_path=/homeassistant/tsun-proxy/logs/ --config_path=/homeassistant/tsun-proxy/ --log_backups=2
|
||||
@@ -1,95 +0,0 @@
|
||||
---
|
||||
configuration:
|
||||
inverters:
|
||||
name: Wechselrichter
|
||||
description: >+
|
||||
Für jeden Wechselrichter muss die Seriennummer des Wechselrichters einer MQTT
|
||||
Definition zugeordnet werden. Dazu wird der entsprechende Konfigurationsblock mit der
|
||||
16-stellige Seriennummer gestartet, so dass alle nachfolgenden Parameter diesem
|
||||
Wechselrichter zugeordnet sind.
|
||||
Weitere wechselrichterspezifische Parameter (z.B. Polling Mode) können im
|
||||
Konfigurationsblock gesetzt werden.
|
||||
|
||||
Die Seriennummer der GEN3 Wechselrichter beginnen mit `R17` und die der GEN3PLUS
|
||||
Wechselrichter mir `Y17`oder `47`!
|
||||
|
||||
Siehe Beispielkonfiguration im Dokumentations-Tab
|
||||
|
||||
tsun.enabled:
|
||||
name: Verbindung zur TSUN Cloud - nur für GEN3-Wechselrichter
|
||||
description: >+
|
||||
Schaltet die Verbindung zur TSUN Cloud ein/aus.
|
||||
Diese Verbindung ist erforderlich, wenn Sie Daten an die TSUN Cloud senden möchten,
|
||||
z.B. um die TSUN-Apps zu nutzen oder Firmware-Updates zu erhalten.
|
||||
|
||||
ein => normaler Proxy-Betrieb.
|
||||
aus => Der Wechselrichter wird vom Internet isoliert.
|
||||
solarman.enabled:
|
||||
name: Verbindung zur Solarman Cloud - nur für GEN3PLUS Wechselrichter
|
||||
description: >+
|
||||
Schaltet die Verbindung zur Solarman Cloud ein/aus.
|
||||
Diese Verbindung ist erforderlich, wenn Sie Daten an die Solarman Cloud senden möchten,
|
||||
z.B. um die Solarman Apps zu nutzen oder Firmware-Updates zu erhalten.
|
||||
|
||||
ein => normaler Proxy-Betrieb.
|
||||
aus => Der Wechselrichter wird vom Internet isoliert.
|
||||
inverters.allow_all:
|
||||
name: Erlaube Verbindungen von sämtlichen Wechselrichtern
|
||||
description: >-
|
||||
Der Proxy akzeptiert normalerweise nur Verbindungen von konfigurierten Wechselrichtern.
|
||||
Schalten Sie dies für Testzwecke und unbekannte Seriennummern ein.
|
||||
mqtt.host:
|
||||
name: MQTT Broker Host
|
||||
description: >-
|
||||
Hostname oder IP-Adresse des MQTT-Brokers. Wenn nicht gesetzt, versucht das Addon, eine Verbindung zum Home Assistant MQTT-Broker herzustellen.
|
||||
mqtt.port:
|
||||
name: MQTT Broker Port
|
||||
description: >-
|
||||
Port des MQTT-Brokers. Wenn nicht gesetzt, versucht das Addon, eine Verbindung zum Home Assistant MQTT-Broker herzustellen.
|
||||
mqtt.user:
|
||||
name: MQTT Broker Benutzer
|
||||
description: >-
|
||||
Benutzer für den MQTT-Broker. Wenn nicht gesetzt, versucht das Addon, eine Verbindung zum Home Assistant MQTT-Broker herzustellen.
|
||||
mqtt.passwd:
|
||||
name: MQTT Broker Passwort
|
||||
description: >-
|
||||
Passwort für den MQTT-Broker. Wenn nicht gesetzt, versucht das Addon, eine Verbindung zum Home Assistant MQTT-Broker herzustellen.
|
||||
ha.auto_conf_prefix:
|
||||
name: MQTT-Präfix für das Abonnieren von Home Assistant-Statusaktualisierungen
|
||||
ha.discovery_prefix:
|
||||
name: MQTT-Präfix für das discovery topic
|
||||
ha.entity_prefix:
|
||||
name: MQTT-Themenpräfix für die Veröffentlichung von Wechselrichterwerten
|
||||
ha.proxy_node_id:
|
||||
name: MQTT-Knoten-ID für die proxy_node_id
|
||||
ha.proxy_unique_id:
|
||||
name: MQTT-eindeutige ID zur Identifizierung einer Proxy-Instanz
|
||||
tsun.host:
|
||||
name: TSUN Cloud Host
|
||||
description: >-
|
||||
Hostname oder IP-Adresse der TSUN-Cloud. Wenn nicht gesetzt, versucht das Addon, eine Verbindung zur Cloud logger.talent-monitoring.com herzustellen.
|
||||
solarman.host:
|
||||
name: Solarman Cloud Host
|
||||
description: >-
|
||||
Hostname oder IP-Adresse der Solarman-Cloud. Wenn nicht gesetzt, versucht das Addon, eine Verbindung zur Cloud iot.talent-monitoring.com herzustellen.
|
||||
gen3plus.at_acl.tsun.allow:
|
||||
name: TSUN GEN3PLUS ACL allow
|
||||
description: >-
|
||||
Liste erlaubter AT-Befehle für TSUN GEN3PLUS
|
||||
gen3plus.at_acl.tsun.block:
|
||||
name: TSUN GEN3 ACL block
|
||||
description: >-
|
||||
Liste blockierter AT-Befehle für TSUN GEN3PLUS
|
||||
gen3plus.at_acl.mqtt.allow:
|
||||
name: MQTT GEN3PLUS ACL allow
|
||||
description: >-
|
||||
Liste erlaubter MQTT-Befehle für GEN3PLUS
|
||||
gen3plus.at_acl.mqtt.block:
|
||||
name: MQTT GEN3PLUS ACL block
|
||||
description: >-
|
||||
Liste blockierter MQTT-Befehle für GEN3PLUS
|
||||
|
||||
|
||||
network:
|
||||
5005/tcp: listening Port für TSUN GEN3 Wechselrichter
|
||||
10000/tcp: listening Port für TSUN GEN3PLUS Wechselrichter
|
||||
@@ -1,95 +0,0 @@
|
||||
---
|
||||
configuration:
|
||||
inverters:
|
||||
name: Inverters
|
||||
description: >+
|
||||
For each GEN3 inverter, the serial number of the inverter must be mapped to an MQTT
|
||||
definition. To do this, the corresponding configuration block is started with
|
||||
16-digit serial number so that all subsequent parameters are assigned
|
||||
to this inverter. Further inverter-specific parameters (e.g. polling mode) can be set
|
||||
in the configuration block
|
||||
|
||||
The serial numbers of all GEN3 inverters start with `R17` and that of the GEN3PLUS
|
||||
inverters with ‘Y17’ or ‘47’!
|
||||
|
||||
For reference see example configuration in Documentation Tab
|
||||
|
||||
tsun.enabled:
|
||||
name: Connection to TSUN Cloud - for GEN3 inverter only
|
||||
description: >+
|
||||
switch on/off connection to the TSUN cloud.
|
||||
This connection is only required if you want send data to the TSUN cloud
|
||||
eg. to use the TSUN APPs or receive firmware updates.
|
||||
|
||||
on => normal proxy operation.
|
||||
off => The Inverter become isolated from Internet.
|
||||
solarman.enabled:
|
||||
name: Connection to Solarman Cloud - for GEN3PLUS inverter only
|
||||
description: >+
|
||||
switch on/off connection to the Solarman cloud.
|
||||
This connection is only required if you want send data to the Solarman cloud
|
||||
eg. to use the Solarman APPs or receive firmware updates.
|
||||
|
||||
on => normal proxy operation.
|
||||
off => The Inverter become isolated from Internet
|
||||
inverters.allow_all:
|
||||
name: Allow all connections from all inverters
|
||||
description: >-
|
||||
The proxy only usually accepts connections from configured inverters.
|
||||
Switch on for test purposes and unknown serial numbers.
|
||||
mqtt.host:
|
||||
name: MQTT Broker Host
|
||||
description: >-
|
||||
Hostname or IP address of the MQTT broker. if not set, the addon will try to connect to the Home Assistant MQTT broker
|
||||
mqtt.port:
|
||||
name: MQTT Broker Port
|
||||
description: >-
|
||||
Port of the MQTT broker. if not set, the addon will try to connect to the Home Assistant MQTT broker
|
||||
mqtt.user:
|
||||
name: MQTT Broker User
|
||||
description: >-
|
||||
User for the MQTT broker. if not set, the addon will try to connect to the Home Assistant MQTT broker
|
||||
mqtt.passwd:
|
||||
name: MQTT Broker Password
|
||||
description: >-
|
||||
Password for the MQTT broker. if not set, the addon will try to connect to the Home Assistant MQTT broker
|
||||
ha.auto_conf_prefix:
|
||||
name: MQTT prefix for subscribing for homeassistant status updates
|
||||
ha.discovery_prefix:
|
||||
name: MQTT prefix for discovery topic
|
||||
ha.entity_prefix:
|
||||
name: MQTT topic prefix for publishing inverter values
|
||||
ha.proxy_node_id:
|
||||
name: MQTT node id, for the proxy_node_id
|
||||
ha.proxy_unique_id:
|
||||
name: MQTT unique id, to identify a proxy instance
|
||||
tsun.host:
|
||||
name: TSUN Cloud Host
|
||||
description: >-
|
||||
Hostname or IP address of the TSUN cloud. if not set, the addon will try to connect to the cloud
|
||||
on logger.talent-monitoring.com
|
||||
solarman.host:
|
||||
name: Solarman Cloud Host
|
||||
description: >-
|
||||
Hostname or IP address of the Solarman cloud. if not set, the addon will try to connect to the cloud
|
||||
on iot.talent-monitoring.com
|
||||
gen3plus.at_acl.tsun.allow:
|
||||
name: TSUN GEN3PLUS ACL allow
|
||||
description: >-
|
||||
List of allowed TSUN GEN3PLUS AT commands
|
||||
gen3plus.at_acl.tsun.block:
|
||||
name: TSUN GEN3 ACL block
|
||||
description: >-
|
||||
List of blocked TSUN GEN3PLUS AT commands
|
||||
gen3plus.at_acl.mqtt.allow:
|
||||
name: MQTT GEN3PLUS ACL allow
|
||||
description: >-
|
||||
List of allowed MQTT GEN3PLUS commands
|
||||
gen3plus.at_acl.mqtt.block:
|
||||
name: MQTT GEN3PLUS ACL block
|
||||
description: >-
|
||||
List of blocked MQTT GEN3PLUS commands
|
||||
|
||||
network:
|
||||
5005/tcp: listening Port for TSUN GEN3 Devices
|
||||
10000/tcp: listening Port for TSUN GEN3PLUS Devices
|
||||
@@ -1,3 +0,0 @@
|
||||
name: TSUN-Proxy
|
||||
url: https://github.com/s-allius/tsun-gen3-proxy/ha_addons
|
||||
maintainer: Stefan Allius
|
||||
@@ -1,111 +0,0 @@
|
||||
name: {{name}}
|
||||
description: {{description}}
|
||||
version: {% if version is defined and version|length %} {{version}} {% else %} {{AppVersion}} {% endif %}
|
||||
image: docker.io/sallius/tsun-gen3-addon
|
||||
url: https://github.com/s-allius/tsun-gen3-proxy
|
||||
slug: {{slug}}
|
||||
advanced: {{advanced}}
|
||||
stage: {{stage}}
|
||||
init: false
|
||||
arch:
|
||||
- aarch64
|
||||
- amd64
|
||||
- armhf
|
||||
- armv7
|
||||
startup: services
|
||||
homeassistant_api: true
|
||||
map:
|
||||
- type: addon_config
|
||||
path: /homeassistant/tsun-proxy
|
||||
read_only: False
|
||||
services:
|
||||
- mqtt:want
|
||||
ports:
|
||||
5005/tcp: 5005
|
||||
10000/tcp: 10000
|
||||
|
||||
# FIXME: we disabled the watchdog due to exceptions in the ha supervisor. See: https://github.com/s-allius/tsun-gen3-proxy/issues/249
|
||||
# watchdog: "http://[HOST]:[PORT:8127]/-/healthy"
|
||||
|
||||
# Definition of parameters in the configuration tab of the addon
|
||||
# parameters are available within the container as /data/options.json
|
||||
# and should become picked up by the proxy - current workaround as a transfer script
|
||||
# TODO: check again for multi hierarchie parameters
|
||||
# TODO: implement direct reading of the configuration file
|
||||
schema:
|
||||
inverters:
|
||||
- serial: str
|
||||
monitor_sn: int?
|
||||
node_id: str
|
||||
suggested_area: str
|
||||
modbus_polling: bool
|
||||
client_mode.host: str?
|
||||
client_mode.port: int?
|
||||
client_mode.forward: bool?
|
||||
#strings: # leider funktioniert es nicht die folgenden 3 parameter im schema aufzulisten. möglicherweise wird die verschachtelung nicht unterstützt.
|
||||
# - string: str
|
||||
# type: str
|
||||
# manufacturer: str
|
||||
# daher diese variante
|
||||
pv1.manufacturer: str?
|
||||
pv1.type: str?
|
||||
pv2.manufacturer: str?
|
||||
pv2.type: str?
|
||||
pv3.manufacturer: str?
|
||||
pv3.type: str?
|
||||
pv4.manufacturer: str?
|
||||
pv4.type: str?
|
||||
pv5.manufacturer: str?
|
||||
pv5.type: str?
|
||||
pv6.manufacturer: str?
|
||||
pv6.type: str?
|
||||
tsun.enabled: bool
|
||||
solarman.enabled: bool
|
||||
inverters.allow_all: bool
|
||||
# optionale parameter
|
||||
# TODO besser strukturieren und vervollständigen
|
||||
mqtt.host: str?
|
||||
mqtt.port: int?
|
||||
mqtt.user: str?
|
||||
mqtt.passwd: password?
|
||||
ha.auto_conf_prefix: str? # suggeriert optionale konfigurationsoption -> es darf jedoch kein default unter "options" angegeben werden
|
||||
ha.discovery_prefix: str? # dito
|
||||
ha.entity_prefix: str? #dito
|
||||
ha.proxy_node_id: str? #dito
|
||||
ha.proxy_unique_id: str? #dito
|
||||
tsun.host: str?
|
||||
solarman.host: str?
|
||||
gen3plus.at_acl.tsun.allow:
|
||||
- str
|
||||
gen3plus.at_acl.tsun.block:
|
||||
- str?
|
||||
gen3plus.at_acl.mqtt.allow:
|
||||
- str
|
||||
gen3plus.at_acl.mqtt.block:
|
||||
- str?
|
||||
|
||||
# set default options for mandatory parameters
|
||||
# for optional parameters do not define any default value in the options dictionary.
|
||||
# If any default value is given, the option becomes a required value.
|
||||
options:
|
||||
inverters:
|
||||
- serial: R17E760702080400
|
||||
node_id: PV-Garage
|
||||
suggested_area: Garage
|
||||
modbus_polling: false
|
||||
# strings:
|
||||
# - string: PV1
|
||||
# type: SF-M18/144550
|
||||
# manufacturer: Shinefar
|
||||
# - string: PV2
|
||||
# type: SF-M18/144550
|
||||
# manufacturer: Shinefar
|
||||
pv1.manufacturer: Shinefar
|
||||
pv1.type: SF-M18/144550
|
||||
pv2.manufacturer: Shinefar
|
||||
pv2.type: SF-M18/144550
|
||||
tsun.enabled: true # set default
|
||||
solarman.enabled: true # set default
|
||||
inverters.allow_all: false # set default
|
||||
gen3plus.at_acl.tsun.allow: ["AT+Z", "AT+UPURL", "AT+SUPDATE"]
|
||||
gen3plus.at_acl.mqtt.allow: ["AT+"]
|
||||
@@ -1,9 +0,0 @@
|
||||
|
||||
{
|
||||
"name": "TSUN-Proxy (Debug)",
|
||||
"description": "MQTT Proxy for TSUN Photovoltaic Inverters with Debug Logging",
|
||||
"version": "debug",
|
||||
"slug": "tsun-proxy-debug",
|
||||
"advanced": true,
|
||||
"stage": "experimental"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
|
||||
{
|
||||
"name": "TSUN-Proxy (Dev)",
|
||||
"description": "MQTT Proxy for TSUN Photovoltaic Inverters",
|
||||
"version": "dev",
|
||||
"slug": "tsun-proxy-dev",
|
||||
"advanced": false,
|
||||
"stage": "experimental"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
{
|
||||
"name": "TSUN-Proxy",
|
||||
"description": "MQTT Proxy for TSUN Photovoltaic Inverters",
|
||||
"slug": "tsun-proxy",
|
||||
"advanced": false,
|
||||
"stage": "stable"
|
||||
}
|
||||
20
proxy.c4
20
proxy.c4
@@ -1,20 +0,0 @@
|
||||
model {
|
||||
extend home.logger.proxy {
|
||||
component webserver 'http server'
|
||||
component inverter 'inverter'
|
||||
component local 'local connection'
|
||||
component remote 'remote connection'
|
||||
component r-ifc 'async-ifc'
|
||||
component l-ifc 'async-ifc'
|
||||
component prot 'Protocol' 'SolarmanV5 or Talent'
|
||||
component config 'config' 'reads the file confg.toml'
|
||||
component mqtt
|
||||
inverter -> local
|
||||
inverter -> remote
|
||||
remote -> r-ifc
|
||||
remote -> prot
|
||||
local -> l-ifc
|
||||
local -> prot
|
||||
prot -> mqtt
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
# pytest.ini or .pytest.ini
|
||||
[pytest]
|
||||
minversion = 8.0
|
||||
addopts = -ra -q --durations=5
|
||||
pythonpath = app/src app/tests ha_addons/ha_addon/rootfs
|
||||
testpaths = app/tests ha_addons/ha_addon/tests
|
||||
asyncio_default_fixture_loop_scope = function
|
||||
asyncio_mode = strict
|
||||
@@ -5,10 +5,6 @@
|
||||
},
|
||||
{
|
||||
"path": "../wiki"
|
||||
},
|
||||
{
|
||||
"name": "ha-addons",
|
||||
"path": "../ha-addons/ha-addons"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
|
||||
Reference in New Issue
Block a user