Compare commits
15 Commits
sonar_esta
...
v0.10.0-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47f7580184 | ||
|
|
b8e44b7379 | ||
|
|
95954fa84e | ||
|
|
3c656e8c63 | ||
|
|
387c014763 | ||
|
|
19916453f2 | ||
|
|
7f81799dd9 | ||
|
|
dc4728122e | ||
|
|
6f35c47254 | ||
|
|
92a5fd22b8 | ||
|
|
f3dd87e03c | ||
|
|
112c7e66f2 | ||
|
|
c7a33b4a35 | ||
|
|
da8f39c401 | ||
|
|
e4ff17e600 |
@@ -1,3 +1,2 @@
|
|||||||
[run]
|
[run]
|
||||||
branch = True
|
branch = True
|
||||||
relative_files = True
|
|
||||||
34
.github/workflows/python-app.yml
vendored
34
.github/workflows/python-app.yml
vendored
@@ -18,11 +18,10 @@ on:
|
|||||||
- '**.dockerfile' # Do no build on *.dockerfile changes
|
- '**.dockerfile' # Do no build on *.dockerfile changes
|
||||||
- '**.sh' # Do no build on *.sh changes
|
- '**.sh' # Do no build on *.sh changes
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main", "dev-*" ]
|
branches: [ "main" ]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: read # allows SonarCloud to decorate PRs with analysis results
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -36,18 +35,7 @@ jobs:
|
|||||||
timezoneLinux: "Europe/Berlin"
|
timezoneLinux: "Europe/Berlin"
|
||||||
timezoneMacos: "Europe/Berlin"
|
timezoneMacos: "Europe/Berlin"
|
||||||
timezoneWindows: "Europe/Berlin"
|
timezoneWindows: "Europe/Berlin"
|
||||||
# - name: Start Mosquitto
|
|
||||||
# uses: namoshek/mosquitto-github-action@v1
|
|
||||||
# with:
|
|
||||||
# version: '1.6'
|
|
||||||
# ports: '1883:1883 8883:8883'
|
|
||||||
# certificates: ${{ github.workspace }}/.ci/tls-certificates
|
|
||||||
# config: ${{ github.workspace }}/.ci/mosquitto.conf
|
|
||||||
# password-file: ${{ github.workspace}}/.ci/mosquitto.passwd
|
|
||||||
# container-name: 'mqtt'
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
@@ -55,28 +43,14 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi
|
pip install flake8 pytest pytest-asyncio
|
||||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||||
- name: Lint with flake8
|
- name: Lint with flake8
|
||||||
run: |
|
run: |
|
||||||
# stop the build if there are Python syntax errors or undefined names
|
# stop the build if there are Python syntax errors or undefined names
|
||||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||||
flake8 --exit-zero --ignore=C901,E121,E123,E126,E133,E226,E241,E242,E704,W503,W504,W505 --format=pylint --output-file=output_flake.txt --exclude=*.pyc app/src/
|
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: |
|
||||||
python -m pytest app --cov=app/src --cov-report=xml
|
python -m pytest app
|
||||||
coverage report
|
|
||||||
- name: Analyze with SonarCloud
|
|
||||||
uses: SonarSource/sonarcloud-github-action@v2.2.0
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
with:
|
|
||||||
projectBaseDir: .
|
|
||||||
args:
|
|
||||||
-Dsonar.projectKey=s-allius_tsun-gen3-proxy
|
|
||||||
-Dsonar.python.coverage.reportPaths=coverage.xml
|
|
||||||
-Dsonar.python.flake8.reportPaths=output_flake.txt
|
|
||||||
# -Dsonar.docker.hadolint.reportPaths=
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"sonarCloudOrganization": "s-allius",
|
|
||||||
"projectKey": "s-allius_tsun-gen3-proxy"
|
|
||||||
}
|
|
||||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@@ -11,12 +11,5 @@
|
|||||||
"python.testing.pytestEnabled": true,
|
"python.testing.pytestEnabled": true,
|
||||||
"flake8.args": [
|
"flake8.args": [
|
||||||
"--extend-exclude=app/tests/*.py system_tests/*.py"
|
"--extend-exclude=app/tests/*.py system_tests/*.py"
|
||||||
],
|
]
|
||||||
"sonarlint.connectedMode.project": {
|
|
||||||
"connectionId": "s-allius",
|
|
||||||
"projectKey": "s-allius_tsun-gen3-proxy"
|
|
||||||
},
|
|
||||||
"files.exclude": {
|
|
||||||
"**/*.pyi": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -7,19 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [unreleased]
|
## [unreleased]
|
||||||
|
|
||||||
- GEN3: don't crash on overwritten msg in the receive buffer
|
|
||||||
- Reading the version string from the image updates it even if the image is re-pulled without re-deployment
|
|
||||||
|
|
||||||
## [0.10.1] - 2024-08-10
|
|
||||||
|
|
||||||
- fix displaying the version string at startup and in HA [#153](https://github.com/s-allius/tsun-gen3-proxy/issues/153)
|
|
||||||
|
|
||||||
## [0.10.0] - 2024-08-09
|
|
||||||
|
|
||||||
- bump aiohttp to version 3.10.2
|
|
||||||
- add SonarQube and code coverage support
|
|
||||||
- don't send MODBUS request when state is note up; adapt timeouts [#141](https://github.com/s-allius/tsun-gen3-proxy/issues/141)
|
|
||||||
- build multi arch images with sboms [#144](https://github.com/s-allius/tsun-gen3-proxy/issues/144)
|
|
||||||
- add timestamp to MQTT topics [#138](https://github.com/s-allius/tsun-gen3-proxy/issues/138)
|
- add timestamp to MQTT topics [#138](https://github.com/s-allius/tsun-gen3-proxy/issues/138)
|
||||||
- improve the message handling, to avoid hangs
|
- improve the message handling, to avoid hangs
|
||||||
- GEN3: allow long timeouts until we received first inverter data (not only device data)
|
- GEN3: allow long timeouts until we received first inverter data (not only device data)
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -10,12 +10,6 @@
|
|||||||
<a href="https://sbtinstruments.github.io/aiomqtt/introduction.html"><img alt="Supported aiomqtt versions" src="https://img.shields.io/badge/aiomqtt-2.2.0-lightblue.svg"></a>
|
<a href="https://sbtinstruments.github.io/aiomqtt/introduction.html"><img alt="Supported aiomqtt versions" src="https://img.shields.io/badge/aiomqtt-2.2.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://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 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 src="https://sonarcloud.io/api/project_badges/measure?project=s-allius_tsun-gen3-proxy&metric=coverage"></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Overview
|
# Overview
|
||||||
@@ -158,14 +152,12 @@ inverters.allow_all = false # True: allow inverters, even if we have no invert
|
|||||||
[inverters."R17xxxxxxxxxxxx1"]
|
[inverters."R17xxxxxxxxxxxx1"]
|
||||||
node_id = 'inv1' # Optional, MQTT replacement for inverters serial number
|
node_id = 'inv1' # Optional, MQTT replacement for inverters serial number
|
||||||
suggested_area = 'roof' # Optional, suggested installation area for home-assistant
|
suggested_area = 'roof' # Optional, suggested installation area for home-assistant
|
||||||
modbus_polling = false # Disable optional MODBUS polling for GEN3 inverter
|
|
||||||
pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
|
|
||||||
[inverters."R17xxxxxxxxxxxx2"]
|
[inverters."R17xxxxxxxxxxxx2"]
|
||||||
node_id = 'inv2' # Optional, MQTT replacement for inverters serial number
|
node_id = 'inv2' # Optional, MQTT replacement for inverters serial number
|
||||||
suggested_area = 'balcony' # Optional, suggested installation area for home-assistant
|
suggested_area = 'balcony' # Optional, suggested installation area for home-assistant
|
||||||
modbus_polling = false # Disable optional MODBUS polling for GEN3 inverter
|
|
||||||
pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
|
|
||||||
@@ -173,7 +165,6 @@ pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module de
|
|||||||
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
|
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
|
||||||
node_id = 'inv_3' # MQTT replacement for inverters serial number
|
node_id = 'inv_3' # MQTT replacement for inverters serial number
|
||||||
suggested_area = 'garage' # suggested installation place for home-assistant
|
suggested_area = 'garage' # suggested installation place for home-assistant
|
||||||
modbus_polling = false # Enable optional MODBUS polling for GEN3PLUS inverter
|
|
||||||
# if your inverter supports SSL connections you must use the client_mode. Pls, uncomment
|
# 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
|
# the next line and configure the fixed IP of your inverter
|
||||||
#client_mode = {host = '192.168.0.1', port = 8899}
|
#client_mode = {host = '192.168.0.1', port = 8899}
|
||||||
@@ -200,12 +191,7 @@ The standard web interface of the inverter can be accessed at `http://<ip-adress
|
|||||||
|
|
||||||
For our purpose, the hidden URL `http://<ip-adress>/config_hide.html` should be called. There you can see and modify the parameters for accessing the cloud. Here we enter the IP address of our proxy and the IP port `10000` for the `Server A Setting` and for `Optional Server Setting`. The second entry is used as a backup in the event of connection problems.
|
For our purpose, the hidden URL `http://<ip-adress>/config_hide.html` should be called. There you can see and modify the parameters for accessing the cloud. Here we enter the IP address of our proxy and the IP port `10000` for the `Server A Setting` and for `Optional Server Setting`. The second entry is used as a backup in the event of connection problems.
|
||||||
|
|
||||||
```txt
|
❗If the IP port is set to 10443 in the inverter configuration, you probably have a firmware with SSL support. In this case, you must use the client-mode configuration.
|
||||||
❗If the IP port is set to 10443 in the inverter configuration, you probably have a firmware with SSL support.
|
|
||||||
In this case, you MUST NOT change the port or the host address, as this may cause the inverter to hang and
|
|
||||||
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.
|
If access to the web interface does not work, it can also be redirected via DNS redirection, as is necessary for the GEN3 inverters.
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ ARG GID
|
|||||||
ARG LOG_LVL
|
ARG LOG_LVL
|
||||||
ARG environment
|
ARG environment
|
||||||
|
|
||||||
|
ENV VERSION=$VERSION
|
||||||
ENV SERVICE_NAME=$SERVICE_NAME
|
ENV SERVICE_NAME=$SERVICE_NAME
|
||||||
ENV UID=$UID
|
ENV UID=$UID
|
||||||
ENV GID=$GID
|
ENV GID=$GID
|
||||||
@@ -62,10 +63,17 @@ RUN python -m pip install --no-cache --no-index /root/wheels/* && \
|
|||||||
COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
|
COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
|
||||||
COPY config .
|
COPY config .
|
||||||
COPY src .
|
COPY src .
|
||||||
RUN echo ${VERSION} > /proxy-version.txt \
|
RUN date > /build-date.txt
|
||||||
&& date > /build-date.txt
|
|
||||||
EXPOSE 5005 8127 10000
|
EXPOSE 5005 8127 10000
|
||||||
|
|
||||||
# command to run on container start
|
# command to run on container start
|
||||||
ENTRYPOINT ["/root/entrypoint.sh"]
|
ENTRYPOINT ["/root/entrypoint.sh"]
|
||||||
CMD [ "python3", "./server.py" ]
|
CMD [ "python3", "./server.py" ]
|
||||||
|
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="TSUN Gen3 Proxy"
|
||||||
|
LABEL org.opencontainers.image.authors="Stefan Allius"
|
||||||
|
LABEL org.opencontainers.image.source=https://github.com/s-allius/tsun-gen3-proxy
|
||||||
|
LABEL org.opencontainers.image.description='This proxy enables a reliable connection between TSUN third generation inverters (eg. TSOL MS600, MS800, MS2000) and an MQTT broker to integrate the inverter into typical home automations.'
|
||||||
|
LABEL org.opencontainers.image.licenses="BSD-3-Clause"
|
||||||
|
LABEL org.opencontainers.image.vendor="Stefan Allius"
|
||||||
|
|||||||
60
app/build.sh
60
app/build.sh
@@ -18,34 +18,58 @@ arr=(${VERSION//./ })
|
|||||||
MAJOR=${arr[0]}
|
MAJOR=${arr[0]}
|
||||||
IMAGE=tsun-gen3-proxy
|
IMAGE=tsun-gen3-proxy
|
||||||
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
if [[ $1 == debug ]] || [[ $1 == dev ]] ;then
|
if [[ $1 == debug ]] || [[ $1 == dev ]] ;then
|
||||||
IMAGE=docker.io/sallius/${IMAGE}
|
IMAGE=docker.io/sallius/${IMAGE}
|
||||||
VERSION=${VERSION}+$1
|
VERSION=${VERSION}-$1
|
||||||
elif [[ $1 == rc ]] || [[ $1 == rel ]] || [[ $1 == preview ]] ;then
|
elif [[ $1 == rc ]] || [[ $1 == rel ]] || [[ $1 == preview ]] ;then
|
||||||
IMAGE=ghcr.io/s-allius/${IMAGE}
|
IMAGE=ghcr.io/s-allius/${IMAGE}
|
||||||
echo 'login to ghcr.io'
|
|
||||||
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
|
|
||||||
else
|
else
|
||||||
echo argument missing!
|
echo argument missing!
|
||||||
echo try: $0 '[debug|dev|preview|rc|rel]'
|
echo try: $0 '[debug|dev|preview|rc|rel]'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export IMAGE
|
|
||||||
export VERSION
|
|
||||||
export BUILD_DATE
|
|
||||||
export BRANCH
|
|
||||||
export MAJOR
|
|
||||||
|
|
||||||
echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE
|
echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE
|
||||||
docker buildx bake -f app/docker-bake.hcl $1
|
if [[ $1 == debug ]];then
|
||||||
|
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=dev --build-arg "LOG_LVL=DEBUG" --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:debug app
|
||||||
|
echo " => pushing ${IMAGE}:debug"
|
||||||
|
docker push -q ${IMAGE}:debug
|
||||||
|
|
||||||
echo -e "${BLUE} => checking docker-compose.yaml file${NC}"
|
elif [[ $1 == dev ]];then
|
||||||
|
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:dev app
|
||||||
|
echo " => pushing ${IMAGE}:dev"
|
||||||
|
docker push -q ${IMAGE}:dev
|
||||||
|
|
||||||
|
elif [[ $1 == preview ]];then
|
||||||
|
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:preview -t ${IMAGE}:${VERSION} app
|
||||||
|
echo 'login to ghcr.io'
|
||||||
|
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
|
||||||
|
echo " => pushing ${IMAGE}:preview"
|
||||||
|
docker push -q ${IMAGE}:preview
|
||||||
|
echo " => pushing ${IMAGE}:${VERSION}"
|
||||||
|
docker push -q ${IMAGE}:${VERSION}
|
||||||
|
|
||||||
|
elif [[ $1 == rc ]];then
|
||||||
|
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:rc -t ${IMAGE}:${VERSION} app
|
||||||
|
echo 'login to ghcr.io'
|
||||||
|
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
|
||||||
|
echo " => pushing ${IMAGE}:rc"
|
||||||
|
docker push -q ${IMAGE}:rc
|
||||||
|
echo " => pushing ${IMAGE}:${VERSION}"
|
||||||
|
docker push -q ${IMAGE}:${VERSION}
|
||||||
|
|
||||||
|
elif [[ $1 == rel ]];then
|
||||||
|
docker build --no-cache --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:latest -t ${IMAGE}:${MAJOR} -t ${IMAGE}:${VERSION} app
|
||||||
|
echo 'login to ghcr.io'
|
||||||
|
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
|
||||||
|
echo " => pushing ${IMAGE}:latest"
|
||||||
|
docker push -q ${IMAGE}:latest
|
||||||
|
echo " => pushing ${IMAGE}:${MAJOR}"
|
||||||
|
docker push -q ${IMAGE}:${MAJOR}
|
||||||
|
echo " => pushing ${IMAGE}:${VERSION}"
|
||||||
|
docker push -q ${IMAGE}:${VERSION}
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ' => checking docker-compose.yaml file'
|
||||||
docker-compose config -q
|
docker-compose config -q
|
||||||
echo
|
echo 'done'
|
||||||
echo -e "${GREEN}${BUILD_DATE} => Version: ${VERSION}${NC} finished"
|
|
||||||
echo
|
|
||||||
|
|||||||
@@ -31,14 +31,12 @@ inverters.allow_all = true # allow inverters, even if we have no inverter mapp
|
|||||||
[inverters."R170000000000001"]
|
[inverters."R170000000000001"]
|
||||||
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
||||||
#suggested_area = '' # Optional, suggested installation area for home-assistant
|
#suggested_area = '' # Optional, suggested installation area for home-assistant
|
||||||
modbus_polling = false # Disable optional MODBUS polling
|
|
||||||
#pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
#pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
#pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
#pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
|
|
||||||
#[inverters."R17xxxxxxxxxxxx2"]
|
#[inverters."R17xxxxxxxxxxxx2"]
|
||||||
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
||||||
#suggested_area = '' # Optional, suggested installation area for home-assistant
|
#suggested_area = '' # Optional, suggested installation area for home-assistant
|
||||||
#modbus_polling = false # Disable optional MODBUS polling
|
|
||||||
#pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
#pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
#pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
#pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
|
|
||||||
@@ -46,7 +44,6 @@ modbus_polling = false # Disable optional MODBUS polling
|
|||||||
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
|
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
|
||||||
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
||||||
#suggested_area = '' # Optional, suggested installation place for home-assistant
|
#suggested_area = '' # Optional, suggested installation place for home-assistant
|
||||||
modbus_polling = true # Enable optional MODBUS polling
|
|
||||||
|
|
||||||
# if your inverter supports SSL connections you must use the client_mode. Pls, uncomment
|
# 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
|
# the next line and configure the fixed IP of your inverter
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
variable "IMAGE" {
|
|
||||||
default = "tsun-gen3-proxy"
|
|
||||||
}
|
|
||||||
variable "VERSION" {
|
|
||||||
default = "0.0.0"
|
|
||||||
}
|
|
||||||
variable "MAJOR" {
|
|
||||||
default = "0"
|
|
||||||
}
|
|
||||||
variable "BUILD_DATE" {
|
|
||||||
default = "dev"
|
|
||||||
}
|
|
||||||
variable "BRANCH" {
|
|
||||||
default = ""
|
|
||||||
}
|
|
||||||
variable "DESCRIPTION" {
|
|
||||||
default = "This proxy enables a reliable connection between TSUN third generation inverters (eg. TSOL MS600, MS800, MS2000) and an MQTT broker to integrate the inverter into typical home automations."
|
|
||||||
}
|
|
||||||
|
|
||||||
target "_common" {
|
|
||||||
context = "app"
|
|
||||||
dockerfile = "Dockerfile"
|
|
||||||
args = {
|
|
||||||
VERSION = "${VERSION}"
|
|
||||||
environment = "production"
|
|
||||||
}
|
|
||||||
attest = [
|
|
||||||
"type =provenance,mode=max",
|
|
||||||
"type =sbom,generator=docker/scout-sbom-indexer:latest"
|
|
||||||
]
|
|
||||||
annotations = [
|
|
||||||
"index:org.opencontainers.image.title=TSUN Gen3 Proxy",
|
|
||||||
"index:org.opencontainers.image.authors=Stefan Allius",
|
|
||||||
"index:org.opencontainers.image.created=${BUILD_DATE}",
|
|
||||||
"index:org.opencontainers.image.version=${VERSION}",
|
|
||||||
"index:org.opencontainers.image.revision=${BRANCH}",
|
|
||||||
"index:org.opencontainers.image.description=${DESCRIPTION}",
|
|
||||||
"index:org.opencontainers.image.licenses=BSD-3-Clause",
|
|
||||||
"index:org.opencontainers.image.source=https://github.com/s-allius/tsun-gen3-proxy"
|
|
||||||
]
|
|
||||||
labels = {
|
|
||||||
"org.opencontainers.image.title" = "TSUN Gen3 Proxy"
|
|
||||||
"org.opencontainers.image.authors" = "Stefan Allius"
|
|
||||||
"org.opencontainers.image.created" = "${BUILD_DATE}"
|
|
||||||
"org.opencontainers.image.version" = "${VERSION}"
|
|
||||||
"org.opencontainers.image.revision" = "${BRANCH}"
|
|
||||||
"org.opencontainers.image.description" = "${DESCRIPTION}"
|
|
||||||
"org.opencontainers.image.licenses" = "BSD-3-Clause"
|
|
||||||
"org.opencontainers.image.source" = "https://github.com/s-allius/tsun-gen3-proxy"
|
|
||||||
}
|
|
||||||
output = [
|
|
||||||
"type=image,push=true"
|
|
||||||
]
|
|
||||||
|
|
||||||
no-cache = false
|
|
||||||
platforms = ["linux/amd64", "linux/arm64", "linux/arm/v7"]
|
|
||||||
}
|
|
||||||
|
|
||||||
target "_debug" {
|
|
||||||
args = {
|
|
||||||
LOG_LVL = "DEBUG"
|
|
||||||
environment = "dev"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
target "_prod" {
|
|
||||||
args = {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
target "debug" {
|
|
||||||
inherits = ["_common", "_debug"]
|
|
||||||
tags = ["${IMAGE}:debug"]
|
|
||||||
}
|
|
||||||
|
|
||||||
target "dev" {
|
|
||||||
inherits = ["_common"]
|
|
||||||
tags = ["${IMAGE}:dev"]
|
|
||||||
}
|
|
||||||
|
|
||||||
target "preview" {
|
|
||||||
inherits = ["_common", "_prod"]
|
|
||||||
tags = ["${IMAGE}:dev", "${IMAGE}:${VERSION}"]
|
|
||||||
}
|
|
||||||
|
|
||||||
target "rc" {
|
|
||||||
inherits = ["_common", "_prod"]
|
|
||||||
tags = ["${IMAGE}:rc", "${IMAGE}:${VERSION}"]
|
|
||||||
}
|
|
||||||
|
|
||||||
target "rel" {
|
|
||||||
inherits = ["_common", "_prod"]
|
|
||||||
tags = ["${IMAGE}:latest", "${IMAGE}:${MAJOR}", "${IMAGE}:${VERSION}"]
|
|
||||||
no-cache = true
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,6 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
user="$(id -u)"
|
user="$(id -u)"
|
||||||
export VERSION=$(cat /proxy-version.txt)
|
|
||||||
|
|
||||||
echo "######################################################"
|
echo "######################################################"
|
||||||
echo "# prepare: '$SERVICE_NAME' Version:$VERSION"
|
echo "# prepare: '$SERVICE_NAME' Version:$VERSION"
|
||||||
echo "# for running with UserID:$UID, GroupID:$GID"
|
echo "# for running with UserID:$UID, GroupID:$GID"
|
||||||
|
|||||||
@@ -188,7 +188,7 @@
|
|||||||
<polygon fill="none" stroke="#000000" points="410.5,-330 410.5,-362 560.5,-362 560.5,-330 410.5,-330"/>
|
<polygon fill="none" stroke="#000000" points="410.5,-330 410.5,-362 560.5,-362 560.5,-330 410.5,-330"/>
|
||||||
<text text-anchor="start" x="453.5455" y="-343" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3</text>
|
<text text-anchor="start" x="453.5455" y="-343" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3</text>
|
||||||
<polygon fill="none" stroke="#000000" points="410.5,-298 410.5,-330 560.5,-330 560.5,-298 410.5,-298"/>
|
<polygon fill="none" stroke="#000000" points="410.5,-298 410.5,-330 560.5,-330 560.5,-298 410.5,-298"/>
|
||||||
<text text-anchor="start" x="420.487" y="-311" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote_stream:ConnectionG3</text>
|
<text text-anchor="start" x="420.487" y="-311" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3</text>
|
||||||
<polygon fill="none" stroke="#000000" points="410.5,-254 410.5,-298 560.5,-298 560.5,-254 410.5,-254"/>
|
<polygon fill="none" stroke="#000000" points="410.5,-254 410.5,-298 560.5,-298 560.5,-254 410.5,-254"/>
|
||||||
<text text-anchor="start" x="466.054" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
<text text-anchor="start" x="466.054" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||||
<text text-anchor="start" x="470.5025" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
<text text-anchor="start" x="470.5025" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
<polygon fill="none" stroke="#000000" points="125.5,-330 125.5,-362 281.5,-362 281.5,-330 125.5,-330"/>
|
<polygon fill="none" stroke="#000000" points="125.5,-330 125.5,-362 281.5,-362 281.5,-330 125.5,-330"/>
|
||||||
<text text-anchor="start" x="168.211" y="-343" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3P</text>
|
<text text-anchor="start" x="168.211" y="-343" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ConnectionG3P</text>
|
||||||
<polygon fill="none" stroke="#000000" points="125.5,-298 125.5,-330 281.5,-330 281.5,-298 125.5,-298"/>
|
<polygon fill="none" stroke="#000000" points="125.5,-298 125.5,-330 281.5,-330 281.5,-298 125.5,-298"/>
|
||||||
<text text-anchor="start" x="135.1525" y="-311" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remote_stream:ConnectionG3P</text>
|
<text text-anchor="start" x="135.1525" y="-311" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">remoteStream:ConnectionG3P</text>
|
||||||
<polygon fill="none" stroke="#000000" points="125.5,-254 125.5,-298 281.5,-298 281.5,-254 125.5,-254"/>
|
<polygon fill="none" stroke="#000000" points="125.5,-254 125.5,-298 281.5,-298 281.5,-254 125.5,-254"/>
|
||||||
<text text-anchor="start" x="184.054" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
<text text-anchor="start" x="184.054" y="-279" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">healthy()</text>
|
||||||
<text text-anchor="start" x="188.5025" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
<text text-anchor="start" x="188.5025" y="-267" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">close()</text>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
@@ -8,9 +8,9 @@
|
|||||||
[IterRegistry||__iter__]^[Message|server_side:bool;header_valid:bool;header_len:unsigned;data_len:unsigned;unique_id;node_id;sug_area;_recv_buffer:bytearray;_send_buffer:bytearray;_forward_buffer:bytearray;db:Infos;new_data:list;state|_read():void<abstract>;close():void;inc_counter():void;dec_counter():void]
|
[IterRegistry||__iter__]^[Message|server_side:bool;header_valid:bool;header_len:unsigned;data_len:unsigned;unique_id;node_id;sug_area;_recv_buffer:bytearray;_send_buffer:bytearray;_forward_buffer:bytearray;db:Infos;new_data:list;state|_read():void<abstract>;close():void;inc_counter():void;dec_counter():void]
|
||||||
[Message]^[Talent|await_conn_resp_cnt;id_str;contact_name;contact_mail;db:InfosG3;mb:Modbus;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;close()]
|
[Message]^[Talent|await_conn_resp_cnt;id_str;contact_name;contact_mail;db:InfosG3;mb:Modbus;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;close()]
|
||||||
[Message]^[SolarmanV5|control;serial;snr;db:InfosG3P;mb:Modbus;switch|msg_unknown();;close()]
|
[Message]^[SolarmanV5|control;serial;snr;db:InfosG3P;mb:Modbus;switch|msg_unknown();;close()]
|
||||||
[Talent]^[ConnectionG3|remote_stream:ConnectionG3|healthy();close()]
|
[Talent]^[ConnectionG3|remoteStream:ConnectionG3|healthy();close()]
|
||||||
[Talent]has-1>[Modbus]
|
[Talent]has-1>[Modbus]
|
||||||
[SolarmanV5]^[ConnectionG3P|remote_stream:ConnectionG3P|healthy();close()]
|
[SolarmanV5]^[ConnectionG3P|remoteStream:ConnectionG3P|healthy();close()]
|
||||||
[SolarmanV5]has-1>[Modbus]
|
[SolarmanV5]has-1>[Modbus]
|
||||||
[AsyncStream|reader;writer;addr;r_addr;l_addr|<async>server_loop();<async>client_loop();<async>loop;disc();close();;__async_read();async_write();__async_forward()]^[ConnectionG3]
|
[AsyncStream|reader;writer;addr;r_addr;l_addr|<async>server_loop();<async>client_loop();<async>loop;disc();close();;__async_read();async_write();__async_forward()]^[ConnectionG3]
|
||||||
[AsyncStream]^[ConnectionG3P]
|
[AsyncStream]^[ConnectionG3P]
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
flake8
|
|
||||||
pytest
|
|
||||||
pytest-asyncio
|
|
||||||
pytest-cov
|
|
||||||
mock
|
|
||||||
coverage
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
aiomqtt==2.2.0
|
aiomqtt==2.2.0
|
||||||
schema==0.7.7
|
schema==0.7.7
|
||||||
aiocron==1.8
|
aiocron==1.8
|
||||||
aiohttp==3.10.2
|
aiohttp==3.9.5
|
||||||
@@ -3,15 +3,10 @@ import logging
|
|||||||
import traceback
|
import traceback
|
||||||
import time
|
import time
|
||||||
from asyncio import StreamReader, StreamWriter
|
from asyncio import StreamReader, StreamWriter
|
||||||
|
from messages import hex_dump_memory, State
|
||||||
from typing import Self
|
from typing import Self
|
||||||
from itertools import count
|
from itertools import count
|
||||||
|
|
||||||
if __name__ == "app.src.async_stream":
|
|
||||||
from app.src.messages import hex_dump_memory, State
|
|
||||||
else: # pragma: no cover
|
|
||||||
from messages import hex_dump_memory, State
|
|
||||||
|
|
||||||
|
|
||||||
import gc
|
import gc
|
||||||
logger = logging.getLogger('conn')
|
logger = logging.getLogger('conn')
|
||||||
|
|
||||||
@@ -22,10 +17,10 @@ class AsyncStream():
|
|||||||
'''maximum processing time for a received msg in sec'''
|
'''maximum processing time for a received msg in sec'''
|
||||||
MAX_START_TIME = 400
|
MAX_START_TIME = 400
|
||||||
'''maximum time without a received msg in sec'''
|
'''maximum time without a received msg in sec'''
|
||||||
MAX_INV_IDLE_TIME = 120
|
MAX_INV_IDLE_TIME = 90
|
||||||
'''maximum time without a received msg from the inverter in sec'''
|
'''maximum time without a received msg from the inverter in sec'''
|
||||||
MAX_DEF_IDLE_TIME = 360
|
MAX_CLOUD_IDLE_TIME = 360
|
||||||
'''maximum default time without a received msg in sec'''
|
'''maximum time without a received msg from cloud side in sec'''
|
||||||
|
|
||||||
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
def __init__(self, reader: StreamReader, writer: StreamWriter,
|
||||||
addr) -> None:
|
addr) -> None:
|
||||||
@@ -42,11 +37,11 @@ class AsyncStream():
|
|||||||
def __timeout(self) -> int:
|
def __timeout(self) -> int:
|
||||||
if self.state == State.init or self.state == State.received:
|
if self.state == State.init or self.state == State.received:
|
||||||
to = self.MAX_START_TIME
|
to = self.MAX_START_TIME
|
||||||
elif self.state == State.up and \
|
|
||||||
self.server_side and self.modbus_polling:
|
|
||||||
to = self.MAX_INV_IDLE_TIME
|
|
||||||
else:
|
else:
|
||||||
to = self.MAX_DEF_IDLE_TIME
|
if self.server_side and self.modbus_polling:
|
||||||
|
to = self.MAX_INV_IDLE_TIME
|
||||||
|
else:
|
||||||
|
to = self.MAX_CLOUD_IDLE_TIME
|
||||||
return to
|
return to
|
||||||
|
|
||||||
async def publish_outstanding_mqtt(self):
|
async def publish_outstanding_mqtt(self):
|
||||||
@@ -72,18 +67,18 @@ class AsyncStream():
|
|||||||
|
|
||||||
# if the server connection closes, we also have to disconnect
|
# if the server connection closes, we also have to disconnect
|
||||||
# the connection to te TSUN cloud
|
# the connection to te TSUN cloud
|
||||||
if self.remote_stream:
|
if self.remoteStream:
|
||||||
logger.info(f'[{self.node_id}:{self.conn_no}] disc client '
|
logger.info(f'[{self.node_id}:{self.conn_no}] disc client '
|
||||||
f'connection: [{self.remote_stream.node_id}:'
|
f'connection: [{self.remoteStream.node_id}:'
|
||||||
f'{self.remote_stream.conn_no}]')
|
f'{self.remoteStream.conn_no}]')
|
||||||
await self.remote_stream.disc()
|
await self.remoteStream.disc()
|
||||||
|
|
||||||
async def client_loop(self, _: str) -> None:
|
async def client_loop(self, addr: str) -> None:
|
||||||
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
||||||
client_stream = await self.remote_stream.loop()
|
clientStream = await self.remoteStream.loop()
|
||||||
logger.info(f'[{client_stream.node_id}:{client_stream.conn_no}] '
|
logger.info(f'[{clientStream.node_id}:{clientStream.conn_no}] '
|
||||||
'Client loop stopped for'
|
'Client loop stopped for'
|
||||||
f' l{client_stream.l_addr}')
|
f' l{clientStream.l_addr}')
|
||||||
|
|
||||||
# if the client connection closes, we don't touch the server
|
# if the client connection closes, we don't touch the server
|
||||||
# connection. Instead we erase the client connection stream,
|
# connection. Instead we erase the client connection stream,
|
||||||
@@ -91,13 +86,13 @@ class AsyncStream():
|
|||||||
# establish a new connection to the TSUN cloud
|
# establish a new connection to the TSUN cloud
|
||||||
|
|
||||||
# erase backlink to inverter
|
# erase backlink to inverter
|
||||||
client_stream.remote_stream = None
|
clientStream.remoteStream = None
|
||||||
|
|
||||||
if self.remote_stream == client_stream:
|
if self.remoteStream == clientStream:
|
||||||
# logging.debug(f'Client l{client_stream.l_addr} refs:'
|
# logging.debug(f'Client l{clientStream.l_addr} refs:'
|
||||||
# f' {gc.get_referrers(client_stream)}')
|
# f' {gc.get_referrers(clientStream)}')
|
||||||
# than erase client connection
|
# than erase client connection
|
||||||
self.remote_stream = None
|
self.remoteStream = None
|
||||||
|
|
||||||
async def loop(self) -> Self:
|
async def loop(self) -> Self:
|
||||||
"""Async loop handler for precessing all received messages"""
|
"""Async loop handler for precessing all received messages"""
|
||||||
@@ -147,7 +142,6 @@ class AsyncStream():
|
|||||||
logger.error(
|
logger.error(
|
||||||
f"Exception for {self.addr}:\n"
|
f"Exception for {self.addr}:\n"
|
||||||
f"{traceback.format_exc()}")
|
f"{traceback.format_exc()}")
|
||||||
await asyncio.sleep(0) # be cooperative to other task
|
|
||||||
|
|
||||||
async def async_write(self, headline: str = 'Transmit to ') -> None:
|
async def async_write(self, headline: str = 'Transmit to ') -> None:
|
||||||
"""Async write handler to transmit the send_buffer"""
|
"""Async write handler to transmit the send_buffer"""
|
||||||
@@ -209,35 +203,35 @@ class AsyncStream():
|
|||||||
if not self._forward_buffer:
|
if not self._forward_buffer:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
if not self.remote_stream:
|
if not self.remoteStream:
|
||||||
await self.async_create_remote()
|
await self.async_create_remote()
|
||||||
if self.remote_stream:
|
if self.remoteStream:
|
||||||
if self.remote_stream._init_new_client_conn():
|
if self.remoteStream._init_new_client_conn():
|
||||||
await self.remote_stream.async_write()
|
await self.remoteStream.async_write()
|
||||||
|
|
||||||
if self.remote_stream:
|
if self.remoteStream:
|
||||||
self.remote_stream._update_header(self._forward_buffer)
|
self.remoteStream._update_header(self._forward_buffer)
|
||||||
hex_dump_memory(logging.INFO,
|
hex_dump_memory(logging.INFO,
|
||||||
f'Forward to {self.remote_stream.addr}:',
|
f'Forward to {self.remoteStream.addr}:',
|
||||||
self._forward_buffer,
|
self._forward_buffer,
|
||||||
len(self._forward_buffer))
|
len(self._forward_buffer))
|
||||||
self.remote_stream.writer.write(self._forward_buffer)
|
self.remoteStream.writer.write(self._forward_buffer)
|
||||||
await self.remote_stream.writer.drain()
|
await self.remoteStream.writer.drain()
|
||||||
self._forward_buffer = bytearray(0)
|
self._forward_buffer = bytearray(0)
|
||||||
|
|
||||||
except OSError as error:
|
except OSError as error:
|
||||||
if self.remote_stream:
|
if self.remoteStream:
|
||||||
rmt = self.remote_stream
|
rmt = self.remoteStream
|
||||||
self.remote_stream = None
|
self.remoteStream = None
|
||||||
logger.error(f'[{rmt.node_id}:{rmt.conn_no}] Fwd: {error} for '
|
logger.error(f'[{rmt.node_id}:{rmt.conn_no}] Fwd: {error} for '
|
||||||
f'l{rmt.l_addr} | r{rmt.r_addr}')
|
f'l{rmt.l_addr} | r{rmt.r_addr}')
|
||||||
await rmt.disc()
|
await rmt.disc()
|
||||||
rmt.close()
|
rmt.close()
|
||||||
|
|
||||||
except RuntimeError as error:
|
except RuntimeError as error:
|
||||||
if self.remote_stream:
|
if self.remoteStream:
|
||||||
rmt = self.remote_stream
|
rmt = self.remoteStream
|
||||||
self.remote_stream = None
|
self.remoteStream = None
|
||||||
logger.info(f'[{rmt.node_id}:{rmt.conn_no}] '
|
logger.info(f'[{rmt.node_id}:{rmt.conn_no}] '
|
||||||
f'Fwd: {error} for {rmt.l_addr}')
|
f'Fwd: {error} for {rmt.l_addr}')
|
||||||
await rmt.disc()
|
await rmt.disc()
|
||||||
|
|||||||
@@ -12,84 +12,81 @@ class Config():
|
|||||||
Read config.toml file and sanitize it with read().
|
Read config.toml file and sanitize it with read().
|
||||||
Get named parts of the config with get()'''
|
Get named parts of the config with get()'''
|
||||||
|
|
||||||
act_config = {}
|
config = {}
|
||||||
def_config = {}
|
def_config = {}
|
||||||
conf_schema = Schema({
|
conf_schema = Schema({
|
||||||
'tsun': {
|
'tsun': {
|
||||||
'enabled': Use(bool),
|
'enabled': Use(bool),
|
||||||
'host': Use(str),
|
'host': Use(str),
|
||||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
|
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
|
||||||
},
|
},
|
||||||
'solarman': {
|
'solarman': {
|
||||||
'enabled': Use(bool),
|
'enabled': Use(bool),
|
||||||
'host': Use(str),
|
'host': Use(str),
|
||||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
|
'port': And(Use(int), lambda n: 1024 <= n <= 65535)
|
||||||
},
|
},
|
||||||
'mqtt': {
|
'mqtt': {
|
||||||
'host': Use(str),
|
'host': Use(str),
|
||||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535),
|
'port': And(Use(int), lambda n: 1024 <= n <= 65535),
|
||||||
'user': 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))
|
'passwd': And(Use(str), Use(lambda s: s if len(s) > 0 else None))
|
||||||
},
|
},
|
||||||
'ha': {
|
'ha': {
|
||||||
'auto_conf_prefix': Use(str),
|
'auto_conf_prefix': Use(str),
|
||||||
'discovery_prefix': Use(str),
|
'discovery_prefix': Use(str),
|
||||||
'entity_prefix': Use(str),
|
'entity_prefix': Use(str),
|
||||||
'proxy_node_id': Use(str),
|
'proxy_node_id': Use(str),
|
||||||
'proxy_unique_id': Use(str)
|
'proxy_unique_id': Use(str)
|
||||||
},
|
},
|
||||||
'gen3plus': {
|
'gen3plus': {
|
||||||
'at_acl': {
|
'at_acl': {
|
||||||
Or('mqtt', 'tsun'): {
|
Or('mqtt', 'tsun'): {
|
||||||
'allow': [str],
|
'allow': [str],
|
||||||
Optional('block', default=[]): [str]
|
Optional('block', default=[]): [str]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
|
||||||
'inverters': {
|
'inverters': {
|
||||||
'allow_all': Use(bool), And(Use(str), lambda s: len(s) == 16): {
|
'allow_all': Use(bool), And(Use(str), lambda s: len(s) == 16): {
|
||||||
Optional('monitor_sn', default=0): Use(int),
|
Optional('monitor_sn', default=0): Use(int),
|
||||||
Optional('node_id', default=""): And(Use(str),
|
Optional('node_id', default=""): And(Use(str),
|
||||||
Use(lambda s: s + '/'
|
Use(lambda s: s + '/'
|
||||||
if len(s) > 0
|
if len(s) > 0 and
|
||||||
and s[-1] != '/'
|
s[-1] != '/' else s)),
|
||||||
else s)),
|
|
||||||
Optional('client_mode'): {
|
Optional('client_mode'): {
|
||||||
'host': Use(str),
|
'host': Use(str),
|
||||||
Optional('port', default=8899):
|
Optional('port', default=8899):
|
||||||
And(Use(int), lambda n: 1024 <= n <= 65535)
|
And(Use(int), lambda n: 1024 <= n <= 65535)
|
||||||
},
|
},
|
||||||
Optional('modbus_polling', default=True): Use(bool),
|
Optional('modbus_polling', default=True): Use(bool),
|
||||||
Optional('suggested_area', default=""): Use(str),
|
Optional('suggested_area', default=""): Use(str),
|
||||||
Optional('sensor_list', default=0x2b0): Use(int),
|
|
||||||
Optional('pv1'): {
|
Optional('pv1'): {
|
||||||
Optional('type'): Use(str),
|
Optional('type'): Use(str),
|
||||||
Optional('manufacturer'): Use(str),
|
Optional('manufacturer'): Use(str),
|
||||||
},
|
},
|
||||||
Optional('pv2'): {
|
Optional('pv2'): {
|
||||||
Optional('type'): Use(str),
|
Optional('type'): Use(str),
|
||||||
Optional('manufacturer'): Use(str),
|
Optional('manufacturer'): Use(str),
|
||||||
},
|
},
|
||||||
Optional('pv3'): {
|
Optional('pv3'): {
|
||||||
Optional('type'): Use(str),
|
Optional('type'): Use(str),
|
||||||
Optional('manufacturer'): Use(str),
|
Optional('manufacturer'): Use(str),
|
||||||
},
|
},
|
||||||
Optional('pv4'): {
|
Optional('pv4'): {
|
||||||
Optional('type'): Use(str),
|
Optional('type'): Use(str),
|
||||||
Optional('manufacturer'): Use(str),
|
Optional('manufacturer'): Use(str),
|
||||||
},
|
},
|
||||||
Optional('pv5'): {
|
Optional('pv5'): {
|
||||||
Optional('type'): Use(str),
|
Optional('type'): Use(str),
|
||||||
Optional('manufacturer'): Use(str),
|
Optional('manufacturer'): Use(str),
|
||||||
},
|
},
|
||||||
Optional('pv6'): {
|
Optional('pv6'): {
|
||||||
Optional('type'): Use(str),
|
Optional('type'): Use(str),
|
||||||
Optional('manufacturer'): Use(str),
|
Optional('manufacturer'): Use(str),
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}
|
}, ignore_extra_keys=True
|
||||||
}, ignore_extra_keys=True
|
)
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def class_init(cls) -> None | str: # pragma: no cover
|
def class_init(cls) -> None | str: # pragma: no cover
|
||||||
@@ -149,17 +146,17 @@ class Config():
|
|||||||
config[key] |= usr_config[key]
|
config[key] |= usr_config[key]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cls.act_config = cls.conf_schema.validate(config)
|
cls.config = cls.conf_schema.validate(config)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
err = f'Config.read: {error}'
|
err = f'Config.read: {error}'
|
||||||
logging.error(err)
|
logging.error(err)
|
||||||
|
|
||||||
# logging.debug(f'Readed config: "{cls.act_config}" ')
|
# logging.debug(f'Readed config: "{cls.config}" ')
|
||||||
|
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
err = f'Config.read: {error}'
|
err = f'Config.read: {error}'
|
||||||
logger.error(err)
|
logger.error(err)
|
||||||
cls.act_config = {}
|
cls.config = {}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|
||||||
@@ -169,12 +166,12 @@ class Config():
|
|||||||
None it returns the complete config dict'''
|
None it returns the complete config dict'''
|
||||||
|
|
||||||
if member:
|
if member:
|
||||||
return cls.act_config.get(member, {})
|
return cls.config.get(member, {})
|
||||||
else:
|
else:
|
||||||
return cls.act_config
|
return cls.config
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_default(cls, member: str) -> bool:
|
def is_default(cls, member: str) -> bool:
|
||||||
'''Check if the member is the default value'''
|
'''Check if the member is the default value'''
|
||||||
|
|
||||||
return cls.act_config.get(member) == cls.def_config.get(member)
|
return cls.config.get(member) == cls.def_config.get(member)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
# import gc
|
||||||
from asyncio import StreamReader, StreamWriter
|
from asyncio import StreamReader, StreamWriter
|
||||||
from async_stream import AsyncStream
|
from async_stream import AsyncStream
|
||||||
from gen3.talent import Talent
|
from gen3.talent import Talent
|
||||||
@@ -14,7 +15,7 @@ class ConnectionG3(AsyncStream, Talent):
|
|||||||
AsyncStream.__init__(self, reader, writer, addr)
|
AsyncStream.__init__(self, reader, writer, addr)
|
||||||
Talent.__init__(self, server_side, id_str)
|
Talent.__init__(self, server_side, id_str)
|
||||||
|
|
||||||
self.remote_stream: 'ConnectionG3' = remote_stream
|
self.remoteStream: 'ConnectionG3' = remote_stream
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Our puplic methods
|
Our puplic methods
|
||||||
@@ -25,10 +26,10 @@ class ConnectionG3(AsyncStream, Talent):
|
|||||||
# logger.info(f'AsyncStream refs: {gc.get_referrers(self)}')
|
# logger.info(f'AsyncStream refs: {gc.get_referrers(self)}')
|
||||||
|
|
||||||
async def async_create_remote(self) -> None:
|
async def async_create_remote(self) -> None:
|
||||||
pass # virtual interface # pragma: no cover
|
pass
|
||||||
|
|
||||||
async def async_publ_mqtt(self) -> None:
|
async def async_publ_mqtt(self) -> None:
|
||||||
pass # virtual interface # pragma: no cover
|
pass
|
||||||
|
|
||||||
def healthy(self) -> bool:
|
def healthy(self) -> bool:
|
||||||
logger.debug('ConnectionG3 healthy()')
|
logger.debug('ConnectionG3 healthy()')
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ class InfosG3(Infos):
|
|||||||
i = elms # abort the loop
|
i = elms # abort the loop
|
||||||
|
|
||||||
elif data_type == 0x41: # 'A' -> Nop ??
|
elif data_type == 0x41: # 'A' -> Nop ??
|
||||||
|
# result = struct.unpack_from('!l', buf, ind)[0]
|
||||||
ind += 0
|
ind += 0
|
||||||
i += 1
|
i += 1
|
||||||
continue
|
continue
|
||||||
@@ -170,17 +171,17 @@ class InfosG3(Infos):
|
|||||||
" not supported")
|
" not supported")
|
||||||
return
|
return
|
||||||
|
|
||||||
yield from self.__store_result(addr, result, info_id, node_id)
|
keys, level, unit, must_incr = self._key_obj(info_id)
|
||||||
i += 1
|
|
||||||
|
|
||||||
def __store_result(self, addr, result, info_id, node_id):
|
if keys:
|
||||||
keys, level, unit, must_incr = self._key_obj(info_id)
|
name, update = self.update_db(keys, must_incr, result)
|
||||||
if keys:
|
yield keys[0], update
|
||||||
name, update = self.update_db(keys, must_incr, result)
|
else:
|
||||||
yield keys[0], update
|
update = False
|
||||||
else:
|
name = str(f'info-id.0x{addr:x}')
|
||||||
update = False
|
|
||||||
name = str(f'info-id.0x{addr:x}')
|
if update:
|
||||||
if update:
|
self.tracer.log(level, f'[{node_id}] GEN3: {name} :'
|
||||||
self.tracer.log(level, f'[{node_id}] GEN3: {name} :'
|
f' {result}{unit}')
|
||||||
f' {result}{unit}')
|
|
||||||
|
i += 1
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ from gen3.connection_g3 import ConnectionG3
|
|||||||
from aiomqtt import MqttCodeError
|
from aiomqtt import MqttCodeError
|
||||||
from infos import Infos
|
from infos import Infos
|
||||||
|
|
||||||
|
# import gc
|
||||||
|
|
||||||
|
# logger = logging.getLogger('conn')
|
||||||
logger_mqtt = logging.getLogger('mqtt')
|
logger_mqtt = logging.getLogger('mqtt')
|
||||||
|
|
||||||
|
|
||||||
@@ -58,10 +60,10 @@ class InverterG3(Inverter, ConnectionG3):
|
|||||||
logging.info(f'[{self.node_id}] Connect to {addr}')
|
logging.info(f'[{self.node_id}] Connect to {addr}')
|
||||||
connect = asyncio.open_connection(host, port)
|
connect = asyncio.open_connection(host, port)
|
||||||
reader, writer = await connect
|
reader, writer = await connect
|
||||||
self.remote_stream = ConnectionG3(reader, writer, addr, self,
|
self.remoteStream = ConnectionG3(reader, writer, addr, self,
|
||||||
False, self.id_str)
|
False, self.id_str)
|
||||||
logging.info(f'[{self.remote_stream.node_id}:'
|
logging.info(f'[{self.remoteStream.node_id}:'
|
||||||
f'{self.remote_stream.conn_no}] '
|
f'{self.remoteStream.conn_no}] '
|
||||||
f'Connected to {addr}')
|
f'Connected to {addr}')
|
||||||
asyncio.create_task(self.client_loop(addr))
|
asyncio.create_task(self.client_loop(addr))
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import struct
|
import struct
|
||||||
import logging
|
import logging
|
||||||
from zoneinfo import ZoneInfo
|
import time
|
||||||
|
import pytz
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from tzlocal import get_localzone
|
from tzlocal import get_localzone
|
||||||
|
|
||||||
@@ -42,10 +43,9 @@ class Control:
|
|||||||
class Talent(Message):
|
class Talent(Message):
|
||||||
MB_START_TIMEOUT = 40
|
MB_START_TIMEOUT = 40
|
||||||
MB_REGULAR_TIMEOUT = 60
|
MB_REGULAR_TIMEOUT = 60
|
||||||
TXT_UNKNOWN_CTRL = 'Unknown Ctrl'
|
|
||||||
|
|
||||||
def __init__(self, server_side: bool, id_str=b''):
|
def __init__(self, server_side: bool, id_str=b''):
|
||||||
super().__init__(server_side, self.send_modbus_cb, mb_timeout=15)
|
super().__init__(server_side, self.send_modbus_cb, mb_timeout=11)
|
||||||
self.await_conn_resp_cnt = 0
|
self.await_conn_resp_cnt = 0
|
||||||
self.id_str = id_str
|
self.id_str = id_str
|
||||||
self.contact_name = b''
|
self.contact_name = b''
|
||||||
@@ -76,7 +76,7 @@ class Talent(Message):
|
|||||||
self.node_id = 'G3' # will be overwritten in __set_serial_no
|
self.node_id = 'G3' # will be overwritten in __set_serial_no
|
||||||
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
|
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
|
||||||
self.mb_timeout = self.MB_REGULAR_TIMEOUT
|
self.mb_timeout = self.MB_REGULAR_TIMEOUT
|
||||||
self.mb_first_timeout = self.MB_START_TIMEOUT
|
self.mb_start_timeout = self.MB_START_TIMEOUT
|
||||||
self.modbus_polling = False
|
self.modbus_polling = False
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@@ -169,6 +169,7 @@ class Talent(Message):
|
|||||||
fnc = self.switch.get(self.msg_id, self.msg_unknown)
|
fnc = self.switch.get(self.msg_id, self.msg_unknown)
|
||||||
logger.info(self.__flow_str(self.server_side, 'forwrd') +
|
logger.info(self.__flow_str(self.server_side, 'forwrd') +
|
||||||
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
|
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
|
||||||
|
return
|
||||||
|
|
||||||
def send_modbus_cb(self, modbus_pdu: bytearray, log_lvl: int, state: str):
|
def send_modbus_cb(self, modbus_pdu: bytearray, log_lvl: int, state: str):
|
||||||
if self.state != State.up:
|
if self.state != State.up:
|
||||||
@@ -177,7 +178,7 @@ class Talent(Message):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.__build_header(0x70, 0x77)
|
self.__build_header(0x70, 0x77)
|
||||||
self._send_buffer += b'\x00\x01\xa3\x28' # magic ?
|
self._send_buffer += b'\x00\x01\xa3\x28' # fixme
|
||||||
self._send_buffer += struct.pack('!B', len(modbus_pdu))
|
self._send_buffer += struct.pack('!B', len(modbus_pdu))
|
||||||
self._send_buffer += modbus_pdu
|
self._send_buffer += modbus_pdu
|
||||||
self.__finish_send_msg()
|
self.__finish_send_msg()
|
||||||
@@ -241,13 +242,17 @@ class Talent(Message):
|
|||||||
def _timestamp(self): # pragma: no cover
|
def _timestamp(self): # pragma: no cover
|
||||||
'''returns timestamp fo the inverter as localtime
|
'''returns timestamp fo the inverter as localtime
|
||||||
since 1.1.1970 in msec'''
|
since 1.1.1970 in msec'''
|
||||||
# convert localtime in epoche
|
if False:
|
||||||
ts = (datetime.now() - datetime(1970, 1, 1)).total_seconds()
|
# utc as epoche
|
||||||
|
ts = time.time()
|
||||||
|
else:
|
||||||
|
# convert localtime in epoche
|
||||||
|
ts = (datetime.now() - datetime(1970, 1, 1)).total_seconds()
|
||||||
return round(ts*1000)
|
return round(ts*1000)
|
||||||
|
|
||||||
def _utcfromts(self, ts: float):
|
def _utcfromts(self, ts: float):
|
||||||
'''converts inverter timestamp into unix time (epoche)'''
|
'''converts inverter timestamp into unix time (epoche)'''
|
||||||
dt = datetime.fromtimestamp(ts/1000, tz=ZoneInfo("UTC")). \
|
dt = datetime.fromtimestamp(ts/1000, pytz.UTC). \
|
||||||
replace(tzinfo=get_localzone())
|
replace(tzinfo=get_localzone())
|
||||||
return dt.timestamp()
|
return dt.timestamp()
|
||||||
|
|
||||||
@@ -292,15 +297,8 @@ class Talent(Message):
|
|||||||
if (buf_len < 5): # enough bytes to read len and id_len?
|
if (buf_len < 5): # enough bytes to read len and id_len?
|
||||||
return
|
return
|
||||||
result = struct.unpack_from('!lB', buf, 0)
|
result = struct.unpack_from('!lB', buf, 0)
|
||||||
msg_len = result[0] # len of complete message
|
len = result[0] # len of complete message
|
||||||
id_len = result[1] # len of variable id string
|
id_len = result[1] # len of variable id string
|
||||||
if id_len > 17:
|
|
||||||
logger.warning(f'len of ID string must == 16 but is {id_len}')
|
|
||||||
self.inc_counter('Invalid_Msg_Format')
|
|
||||||
|
|
||||||
# erase broken recv buffer
|
|
||||||
self._recv_buffer = bytearray()
|
|
||||||
return
|
|
||||||
|
|
||||||
hdr_len = 5+id_len+2
|
hdr_len = 5+id_len+2
|
||||||
|
|
||||||
@@ -313,9 +311,10 @@ class Talent(Message):
|
|||||||
self.id_str = result[0]
|
self.id_str = result[0]
|
||||||
self.ctrl = Control(result[1])
|
self.ctrl = Control(result[1])
|
||||||
self.msg_id = result[2]
|
self.msg_id = result[2]
|
||||||
self.data_len = msg_len-id_len-3
|
self.data_len = len-id_len-3
|
||||||
self.header_len = hdr_len
|
self.header_len = hdr_len
|
||||||
self.header_valid = True
|
self.header_valid = True
|
||||||
|
return
|
||||||
|
|
||||||
def __build_header(self, ctrl, msg_id=None) -> None:
|
def __build_header(self, ctrl, msg_id=None) -> None:
|
||||||
if not msg_id:
|
if not msg_id:
|
||||||
@@ -361,8 +360,9 @@ class Talent(Message):
|
|||||||
self.await_conn_resp_cnt -= 1
|
self.await_conn_resp_cnt -= 1
|
||||||
else:
|
else:
|
||||||
self.forward()
|
self.forward()
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
logger.warning(self.TXT_UNKNOWN_CTRL)
|
logger.warning('Unknown Ctrl')
|
||||||
self.inc_counter('Unknown_Ctrl')
|
self.inc_counter('Unknown_Ctrl')
|
||||||
self.forward()
|
self.forward()
|
||||||
|
|
||||||
@@ -388,6 +388,10 @@ class Talent(Message):
|
|||||||
if self.data_len == 0:
|
if self.data_len == 0:
|
||||||
if self.state == State.up:
|
if self.state == State.up:
|
||||||
self.state = State.pend # block MODBUS cmds
|
self.state = State.pend # block MODBUS cmds
|
||||||
|
if (self.modbus_polling):
|
||||||
|
self.mb_timer.start(self.mb_start_timeout)
|
||||||
|
self.db.set_db_def_value(Register.POLLING_INTERVAL,
|
||||||
|
self.mb_timeout)
|
||||||
|
|
||||||
ts = self._timestamp()
|
ts = self._timestamp()
|
||||||
logger.debug(f'time: {ts:08x}')
|
logger.debug(f'time: {ts:08x}')
|
||||||
@@ -405,7 +409,7 @@ class Talent(Message):
|
|||||||
f' offset: {self.ts_offset}')
|
f' offset: {self.ts_offset}')
|
||||||
return # ignore received response
|
return # ignore received response
|
||||||
else:
|
else:
|
||||||
logger.warning(self.TXT_UNKNOWN_CTRL)
|
logger.warning('Unknown Ctrl')
|
||||||
self.inc_counter('Unknown_Ctrl')
|
self.inc_counter('Unknown_Ctrl')
|
||||||
|
|
||||||
self.forward()
|
self.forward()
|
||||||
@@ -439,7 +443,7 @@ class Talent(Message):
|
|||||||
elif self.ctrl.is_resp():
|
elif self.ctrl.is_resp():
|
||||||
return # ignore received response
|
return # ignore received response
|
||||||
else:
|
else:
|
||||||
logger.warning(self.TXT_UNKNOWN_CTRL)
|
logger.warning('Unknown Ctrl')
|
||||||
self.inc_counter('Unknown_Ctrl')
|
self.inc_counter('Unknown_Ctrl')
|
||||||
|
|
||||||
self.forward()
|
self.forward()
|
||||||
@@ -451,15 +455,11 @@ class Talent(Message):
|
|||||||
self.__finish_send_msg()
|
self.__finish_send_msg()
|
||||||
self.__process_data()
|
self.__process_data()
|
||||||
self.state = State.up # allow MODBUS cmds
|
self.state = State.up # allow MODBUS cmds
|
||||||
if (self.modbus_polling):
|
|
||||||
self.mb_timer.start(self.mb_first_timeout)
|
|
||||||
self.db.set_db_def_value(Register.POLLING_INTERVAL,
|
|
||||||
self.mb_timeout)
|
|
||||||
|
|
||||||
elif self.ctrl.is_resp():
|
elif self.ctrl.is_resp():
|
||||||
return # ignore received response
|
return # ignore received response
|
||||||
else:
|
else:
|
||||||
logger.warning(self.TXT_UNKNOWN_CTRL)
|
logger.warning('Unknown Ctrl')
|
||||||
self.inc_counter('Unknown_Ctrl')
|
self.inc_counter('Unknown_Ctrl')
|
||||||
|
|
||||||
self.forward()
|
self.forward()
|
||||||
@@ -477,9 +477,9 @@ class Talent(Message):
|
|||||||
if self.ctrl.is_req():
|
if self.ctrl.is_req():
|
||||||
self.inc_counter('OTA_Start_Msg')
|
self.inc_counter('OTA_Start_Msg')
|
||||||
elif self.ctrl.is_ind():
|
elif self.ctrl.is_ind():
|
||||||
pass # Ok, nothing to do
|
pass
|
||||||
else:
|
else:
|
||||||
logger.warning(self.TXT_UNKNOWN_CTRL)
|
logger.warning('Unknown Ctrl')
|
||||||
self.inc_counter('Unknown_Ctrl')
|
self.inc_counter('Unknown_Ctrl')
|
||||||
self.forward()
|
self.forward()
|
||||||
|
|
||||||
@@ -490,24 +490,27 @@ class Talent(Message):
|
|||||||
result = struct.unpack_from('!lBB', self._recv_buffer,
|
result = struct.unpack_from('!lBB', self._recv_buffer,
|
||||||
self.header_len)
|
self.header_len)
|
||||||
modbus_len = result[1]
|
modbus_len = result[1]
|
||||||
|
# logger.debug(f'Ref: {result[0]}')
|
||||||
|
# logger.debug(f'Modbus MsgLen: {modbus_len} Func:{result[2]}')
|
||||||
return msg_hdr_len, modbus_len
|
return msg_hdr_len, modbus_len
|
||||||
|
|
||||||
def get_modbus_log_lvl(self) -> int:
|
def get_modbus_log_lvl(self) -> int:
|
||||||
if self.ctrl.is_req():
|
if self.ctrl.is_req():
|
||||||
return logging.INFO
|
return logging.INFO
|
||||||
elif self.ctrl.is_ind() and self.server_side:
|
elif self.ctrl.is_ind():
|
||||||
return self.mb.last_log_lvl
|
if self.server_side:
|
||||||
|
return self.mb.last_log_lvl
|
||||||
return logging.WARNING
|
return logging.WARNING
|
||||||
|
|
||||||
def msg_modbus(self):
|
def msg_modbus(self):
|
||||||
hdr_len, _ = self.parse_modbus_header()
|
hdr_len, modbus_len = self.parse_modbus_header()
|
||||||
data = self._recv_buffer[self.header_len:
|
data = self._recv_buffer[self.header_len:
|
||||||
self.header_len+self.data_len]
|
self.header_len+self.data_len]
|
||||||
|
|
||||||
if self.ctrl.is_req():
|
if self.ctrl.is_req():
|
||||||
if self.remote_stream.mb.recv_req(data[hdr_len:],
|
if self.remoteStream.mb.recv_req(data[hdr_len:],
|
||||||
self.remote_stream.
|
self.remoteStream.
|
||||||
msg_forward):
|
msg_forward):
|
||||||
self.inc_counter('Modbus_Command')
|
self.inc_counter('Modbus_Command')
|
||||||
else:
|
else:
|
||||||
self.inc_counter('Invalid_Msg_Format')
|
self.inc_counter('Invalid_Msg_Format')
|
||||||
@@ -527,7 +530,7 @@ class Talent(Message):
|
|||||||
self.new_data[key] = True
|
self.new_data[key] = True
|
||||||
self.modbus_elms += 1 # count for unit tests
|
self.modbus_elms += 1 # count for unit tests
|
||||||
else:
|
else:
|
||||||
logger.warning(self.TXT_UNKNOWN_CTRL)
|
logger.warning('Unknown Ctrl')
|
||||||
self.inc_counter('Unknown_Ctrl')
|
self.inc_counter('Unknown_Ctrl')
|
||||||
self.forward()
|
self.forward()
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
|
# import gc
|
||||||
from asyncio import StreamReader, StreamWriter
|
from asyncio import StreamReader, StreamWriter
|
||||||
|
from async_stream import AsyncStream
|
||||||
if __name__ == "app.src.gen3plus.connection_g3p":
|
from gen3plus.solarman_v5 import SolarmanV5
|
||||||
from app.src.async_stream import AsyncStream
|
|
||||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
|
||||||
else: # pragma: no cover
|
|
||||||
from async_stream import AsyncStream
|
|
||||||
from gen3plus.solarman_v5 import SolarmanV5
|
|
||||||
|
|
||||||
logger = logging.getLogger('conn')
|
logger = logging.getLogger('conn')
|
||||||
|
|
||||||
@@ -20,7 +16,7 @@ class ConnectionG3P(AsyncStream, SolarmanV5):
|
|||||||
AsyncStream.__init__(self, reader, writer, addr)
|
AsyncStream.__init__(self, reader, writer, addr)
|
||||||
SolarmanV5.__init__(self, server_side, client_mode)
|
SolarmanV5.__init__(self, server_side, client_mode)
|
||||||
|
|
||||||
self.remote_stream: 'ConnectionG3P' = remote_stream
|
self.remoteStream: 'ConnectionG3P' = remote_stream
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Our puplic methods
|
Our puplic methods
|
||||||
@@ -31,10 +27,10 @@ class ConnectionG3P(AsyncStream, SolarmanV5):
|
|||||||
# logger.info(f'AsyncStream refs: {gc.get_referrers(self)}')
|
# logger.info(f'AsyncStream refs: {gc.get_referrers(self)}')
|
||||||
|
|
||||||
async def async_create_remote(self) -> None:
|
async def async_create_remote(self) -> None:
|
||||||
pass # virtual interface # pragma: no cover
|
pass
|
||||||
|
|
||||||
async def async_publ_mqtt(self) -> None:
|
async def async_publ_mqtt(self) -> None:
|
||||||
pass # virtual interface # pragma: no cover
|
pass
|
||||||
|
|
||||||
def healthy(self) -> bool:
|
def healthy(self) -> bool:
|
||||||
logger.debug('ConnectionG3P healthy()')
|
logger.debug('ConnectionG3P healthy()')
|
||||||
|
|||||||
@@ -20,11 +20,9 @@ class RegisterMap:
|
|||||||
0x4102001c: {'reg': Register.SIGNAL_STRENGTH, 'fmt': '<B', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501
|
0x4102001c: {'reg': Register.SIGNAL_STRENGTH, 'fmt': '<B', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501
|
||||||
0x4102001e: {'reg': Register.CHIP_MODEL, 'fmt': '!40s'}, # noqa: E501
|
0x4102001e: {'reg': Register.CHIP_MODEL, 'fmt': '!40s'}, # noqa: E501
|
||||||
0x4102004c: {'reg': Register.IP_ADDRESS, 'fmt': '!16s'}, # noqa: E501
|
0x4102004c: {'reg': Register.IP_ADDRESS, 'fmt': '!16s'}, # noqa: E501
|
||||||
0x4102005f: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'eval': "f'{result:04x}'"}, # noqa: E501
|
|
||||||
0x41020064: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501
|
0x41020064: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501
|
||||||
|
|
||||||
0x4201000c: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'eval': "f'{result:04x}'"}, # noqa: E501
|
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501
|
||||||
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501, or packet number
|
|
||||||
0x42010020: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
|
0x42010020: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
|
||||||
0x420100c0: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
|
0x420100c0: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||||
0x420100d0: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501
|
0x420100d0: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501
|
||||||
@@ -120,7 +118,15 @@ class InfosG3P(Infos):
|
|||||||
if not isinstance(row, dict):
|
if not isinstance(row, dict):
|
||||||
continue
|
continue
|
||||||
info_id = row['reg']
|
info_id = row['reg']
|
||||||
result = self.__get_value(buf, addr, row)
|
fmt = row['fmt']
|
||||||
|
res = struct.unpack_from(fmt, buf, addr)
|
||||||
|
result = res[0]
|
||||||
|
if isinstance(result, (bytearray, bytes)):
|
||||||
|
result = result.decode().split('\x00')[0]
|
||||||
|
if 'eval' in row:
|
||||||
|
result = eval(row['eval'])
|
||||||
|
if 'ratio' in row:
|
||||||
|
result = round(result * row['ratio'], 2)
|
||||||
|
|
||||||
keys, level, unit, must_incr = self._key_obj(info_id)
|
keys, level, unit, must_incr = self._key_obj(info_id)
|
||||||
|
|
||||||
@@ -134,16 +140,3 @@ class InfosG3P(Infos):
|
|||||||
if update:
|
if update:
|
||||||
self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}'
|
self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}'
|
||||||
f' : {result}{unit}')
|
f' : {result}{unit}')
|
||||||
|
|
||||||
def __get_value(self, buf, idx, row):
|
|
||||||
'''Get a value from buf and interpret as in row'''
|
|
||||||
fmt = row['fmt']
|
|
||||||
res = struct.unpack_from(fmt, buf, idx)
|
|
||||||
result = res[0]
|
|
||||||
if isinstance(result, (bytearray, bytes)):
|
|
||||||
result = result.decode().split('\x00')[0]
|
|
||||||
if 'eval' in row:
|
|
||||||
result = eval(row['eval'])
|
|
||||||
if 'ratio' in row:
|
|
||||||
result = round(result * row['ratio'], 2)
|
|
||||||
return result
|
|
||||||
|
|||||||
@@ -3,20 +3,15 @@ import traceback
|
|||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
from asyncio import StreamReader, StreamWriter
|
from asyncio import StreamReader, StreamWriter
|
||||||
|
from config import Config
|
||||||
|
from inverter import Inverter
|
||||||
|
from gen3plus.connection_g3p import ConnectionG3P
|
||||||
from aiomqtt import MqttCodeError
|
from aiomqtt import MqttCodeError
|
||||||
|
from infos import Infos
|
||||||
|
|
||||||
if __name__ == "app.src.gen3plus.inverter_g3p":
|
# import gc
|
||||||
from app.src.config import Config
|
|
||||||
from app.src.inverter import Inverter
|
|
||||||
from app.src.gen3plus.connection_g3p import ConnectionG3P
|
|
||||||
from app.src.infos import Infos
|
|
||||||
else: # pragma: no cover
|
|
||||||
from config import Config
|
|
||||||
from inverter import Inverter
|
|
||||||
from gen3plus.connection_g3p import ConnectionG3P
|
|
||||||
from infos import Infos
|
|
||||||
|
|
||||||
|
|
||||||
|
# logger = logging.getLogger('conn')
|
||||||
logger_mqtt = logging.getLogger('mqtt')
|
logger_mqtt = logging.getLogger('mqtt')
|
||||||
|
|
||||||
|
|
||||||
@@ -67,11 +62,11 @@ class InverterG3P(Inverter, ConnectionG3P):
|
|||||||
logging.info(f'[{self.node_id}] Connect to {addr}')
|
logging.info(f'[{self.node_id}] Connect to {addr}')
|
||||||
connect = asyncio.open_connection(host, port)
|
connect = asyncio.open_connection(host, port)
|
||||||
reader, writer = await connect
|
reader, writer = await connect
|
||||||
self.remote_stream = ConnectionG3P(reader, writer, addr, self,
|
self.remoteStream = ConnectionG3P(reader, writer, addr, self,
|
||||||
server_side=False,
|
server_side=False,
|
||||||
client_mode=False)
|
client_mode=False)
|
||||||
logging.info(f'[{self.remote_stream.node_id}:'
|
logging.info(f'[{self.remoteStream.node_id}:'
|
||||||
f'{self.remote_stream.conn_no}] '
|
f'{self.remoteStream.conn_no}] '
|
||||||
f'Connected to {addr}')
|
f'Connected to {addr}')
|
||||||
asyncio.create_task(self.client_loop(addr))
|
asyncio.create_task(self.client_loop(addr))
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import struct
|
import struct
|
||||||
|
# import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -18,6 +19,7 @@ else: # pragma: no cover
|
|||||||
from my_timer import Timer
|
from my_timer import Timer
|
||||||
from gen3plus.infos_g3p import InfosG3P
|
from gen3plus.infos_g3p import InfosG3P
|
||||||
from infos import Register
|
from infos import Register
|
||||||
|
# import traceback
|
||||||
|
|
||||||
logger = logging.getLogger('msg')
|
logger = logging.getLogger('msg')
|
||||||
|
|
||||||
@@ -57,11 +59,9 @@ class SolarmanV5(Message):
|
|||||||
'''regular Modbus polling time in server mode'''
|
'''regular Modbus polling time in server mode'''
|
||||||
MB_CLIENT_DATA_UP = 30
|
MB_CLIENT_DATA_UP = 30
|
||||||
'''Data up time in client mode'''
|
'''Data up time in client mode'''
|
||||||
HDR_FMT = '<BLLL'
|
|
||||||
'''format string for packing of the header'''
|
|
||||||
|
|
||||||
def __init__(self, server_side: bool, client_mode: bool):
|
def __init__(self, server_side: bool, client_mode: bool):
|
||||||
super().__init__(server_side, self.send_modbus_cb, mb_timeout=8)
|
super().__init__(server_side, self.send_modbus_cb, mb_timeout=5)
|
||||||
|
|
||||||
self.header_len = 11 # overwrite construcor in class Message
|
self.header_len = 11 # overwrite construcor in class Message
|
||||||
self.control = 0
|
self.control = 0
|
||||||
@@ -135,10 +135,9 @@ class SolarmanV5(Message):
|
|||||||
self.node_id = 'G3P' # will be overwritten in __set_serial_no
|
self.node_id = 'G3P' # will be overwritten in __set_serial_no
|
||||||
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
|
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
|
||||||
self.mb_timeout = self.MB_REGULAR_TIMEOUT
|
self.mb_timeout = self.MB_REGULAR_TIMEOUT
|
||||||
self.mb_first_timeout = self.MB_START_TIMEOUT
|
self.mb_start_timeout = self.MB_START_TIMEOUT
|
||||||
'''timer value for next Modbus polling request'''
|
'''timer value for next Modbus polling request'''
|
||||||
self.modbus_polling = False
|
self.modbus_polling = False
|
||||||
self.sensor_list = 0x0000
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Our puplic methods
|
Our puplic methods
|
||||||
@@ -172,7 +171,7 @@ class SolarmanV5(Message):
|
|||||||
self.db.set_db_def_value(Register.POLLING_INTERVAL,
|
self.db.set_db_def_value(Register.POLLING_INTERVAL,
|
||||||
self.mb_timeout)
|
self.mb_timeout)
|
||||||
self.db.set_db_def_value(Register.HEARTBEAT_INTERVAL,
|
self.db.set_db_def_value(Register.HEARTBEAT_INTERVAL,
|
||||||
120)
|
120) # fixme
|
||||||
self.new_data['controller'] = True
|
self.new_data['controller'] = True
|
||||||
|
|
||||||
self.state = State.up
|
self.state = State.up
|
||||||
@@ -183,23 +182,16 @@ class SolarmanV5(Message):
|
|||||||
if self.state is not State.up:
|
if self.state is not State.up:
|
||||||
self.state = State.up
|
self.state = State.up
|
||||||
if (self.modbus_polling):
|
if (self.modbus_polling):
|
||||||
self.mb_timer.start(self.mb_first_timeout)
|
self.mb_timer.start(self.mb_start_timeout)
|
||||||
self.db.set_db_def_value(Register.POLLING_INTERVAL,
|
self.db.set_db_def_value(Register.POLLING_INTERVAL,
|
||||||
self.mb_timeout)
|
self.mb_timeout)
|
||||||
|
|
||||||
def __set_config_parms(self, inv: dict):
|
|
||||||
'''init connection with params from the configuration'''
|
|
||||||
self.node_id = inv['node_id']
|
|
||||||
self.sug_area = inv['suggested_area']
|
|
||||||
self.modbus_polling = inv['modbus_polling']
|
|
||||||
self.sensor_list = inv['sensor_list']
|
|
||||||
|
|
||||||
def __set_serial_no(self, snr: int):
|
def __set_serial_no(self, snr: int):
|
||||||
'''check the serial number and configure the inverter connection'''
|
|
||||||
serial_no = str(snr)
|
serial_no = str(snr)
|
||||||
if self.unique_id == serial_no:
|
if self.unique_id == serial_no:
|
||||||
logger.debug(f'SerialNo: {serial_no}')
|
logger.debug(f'SerialNo: {serial_no}')
|
||||||
else:
|
else:
|
||||||
|
found = False
|
||||||
inverters = Config.get('inverters')
|
inverters = Config.get('inverters')
|
||||||
# logger.debug(f'Inverters: {inverters}')
|
# logger.debug(f'Inverters: {inverters}')
|
||||||
|
|
||||||
@@ -207,11 +199,14 @@ class SolarmanV5(Message):
|
|||||||
# logger.debug(f'key: {key} -> {inv}')
|
# logger.debug(f'key: {key} -> {inv}')
|
||||||
if (type(inv) is dict and 'monitor_sn' in inv
|
if (type(inv) is dict and 'monitor_sn' in inv
|
||||||
and inv['monitor_sn'] == snr):
|
and inv['monitor_sn'] == snr):
|
||||||
self.__set_config_parms(inv)
|
found = True
|
||||||
self.db.set_pv_module_details(inv)
|
self.node_id = inv['node_id']
|
||||||
|
self.sug_area = inv['suggested_area']
|
||||||
|
self.modbus_polling = inv['modbus_polling']
|
||||||
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
||||||
break
|
self.db.set_pv_module_details(inv)
|
||||||
else:
|
|
||||||
|
if not found:
|
||||||
self.node_id = ''
|
self.node_id = ''
|
||||||
self.sug_area = ''
|
self.sug_area = ''
|
||||||
if 'allow_all' not in inverters or not inverters['allow_all']:
|
if 'allow_all' not in inverters or not inverters['allow_all']:
|
||||||
@@ -219,7 +214,7 @@ class SolarmanV5(Message):
|
|||||||
self.unique_id = None
|
self.unique_id = None
|
||||||
logger.warning(f'ignore message from unknow inverter! (SerialNo: {serial_no})') # noqa: E501
|
logger.warning(f'ignore message from unknow inverter! (SerialNo: {serial_no})') # noqa: E501
|
||||||
return
|
return
|
||||||
logger.warning(f'SerialNo {serial_no} not known but accepted!')
|
logger.debug(f'SerialNo {serial_no} not known but accepted!')
|
||||||
|
|
||||||
self.unique_id = serial_no
|
self.unique_id = serial_no
|
||||||
|
|
||||||
@@ -232,25 +227,23 @@ class SolarmanV5(Message):
|
|||||||
|
|
||||||
if self.header_valid and len(self._recv_buffer) >= \
|
if self.header_valid and len(self._recv_buffer) >= \
|
||||||
(self.header_len + self.data_len+2):
|
(self.header_len + self.data_len+2):
|
||||||
self.__process_complete_received_msg()
|
log_lvl = self.log_lvl.get(self.control, logging.WARNING)
|
||||||
|
if callable(log_lvl):
|
||||||
|
log_lvl = log_lvl()
|
||||||
|
hex_dump_memory(log_lvl, f'Received from {self.addr}:',
|
||||||
|
self._recv_buffer, self.header_len +
|
||||||
|
self.data_len+2)
|
||||||
|
if self.__trailer_is_ok(self._recv_buffer, self.header_len
|
||||||
|
+ self.data_len + 2):
|
||||||
|
if self.state == State.init:
|
||||||
|
self.state = State.received
|
||||||
|
|
||||||
|
self.__set_serial_no(self.snr)
|
||||||
|
self.__dispatch_msg()
|
||||||
self.__flush_recv_msg()
|
self.__flush_recv_msg()
|
||||||
else:
|
else:
|
||||||
return 0 # wait 0s before sending a response
|
return 0 # wait 0s before sending a response
|
||||||
|
|
||||||
def __process_complete_received_msg(self):
|
|
||||||
log_lvl = self.log_lvl.get(self.control, logging.WARNING)
|
|
||||||
if callable(log_lvl):
|
|
||||||
log_lvl = log_lvl()
|
|
||||||
hex_dump_memory(log_lvl, f'Received from {self.addr}:',
|
|
||||||
self._recv_buffer, self.header_len +
|
|
||||||
self.data_len+2)
|
|
||||||
if self.__trailer_is_ok(self._recv_buffer, self.header_len
|
|
||||||
+ self.data_len + 2):
|
|
||||||
if self.state == State.init:
|
|
||||||
self.state = State.received
|
|
||||||
self.__set_serial_no(self.snr)
|
|
||||||
self.__dispatch_msg()
|
|
||||||
|
|
||||||
def forward(self, buffer, buflen) -> None:
|
def forward(self, buffer, buflen) -> None:
|
||||||
'''add the actual receive msg to the forwarding queue'''
|
'''add the actual receive msg to the forwarding queue'''
|
||||||
if self.no_forwarding:
|
if self.no_forwarding:
|
||||||
@@ -265,6 +258,7 @@ class SolarmanV5(Message):
|
|||||||
logger.info(self.__flow_str(self.server_side, 'forwrd') +
|
logger.info(self.__flow_str(self.server_side, 'forwrd') +
|
||||||
f' Ctl: {int(self.control):#04x}'
|
f' Ctl: {int(self.control):#04x}'
|
||||||
f' Msg: {fnc.__name__!r}')
|
f' Msg: {fnc.__name__!r}')
|
||||||
|
return
|
||||||
|
|
||||||
def _init_new_client_conn(self) -> bool:
|
def _init_new_client_conn(self) -> bool:
|
||||||
return False
|
return False
|
||||||
@@ -318,6 +312,7 @@ class SolarmanV5(Message):
|
|||||||
self._recv_buffer = bytearray()
|
self._recv_buffer = bytearray()
|
||||||
return
|
return
|
||||||
self.header_valid = True
|
self.header_valid = True
|
||||||
|
return
|
||||||
|
|
||||||
def __trailer_is_ok(self, buf: bytes, buf_len: int) -> bool:
|
def __trailer_is_ok(self, buf: bytes, buf_len: int) -> bool:
|
||||||
crc = buf[self.data_len+11]
|
crc = buf[self.data_len+11]
|
||||||
@@ -410,7 +405,7 @@ class SolarmanV5(Message):
|
|||||||
return
|
return
|
||||||
self.__build_header(0x4510)
|
self.__build_header(0x4510)
|
||||||
self._send_buffer += struct.pack('<BHLLL', self.MB_RTU_CMD,
|
self._send_buffer += struct.pack('<BHLLL', self.MB_RTU_CMD,
|
||||||
self.sensor_list, 0, 0, 0)
|
0x2b0, 0, 0, 0)
|
||||||
self._send_buffer += pdu
|
self._send_buffer += pdu
|
||||||
self.__finish_send_msg()
|
self.__finish_send_msg()
|
||||||
hex_dump_memory(log_lvl, f'Send Modbus {state}:{self.addr}:',
|
hex_dump_memory(log_lvl, f'Send Modbus {state}:{self.addr}:',
|
||||||
@@ -441,15 +436,15 @@ class SolarmanV5(Message):
|
|||||||
return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \
|
return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \
|
||||||
cmd.startswith(tuple(self.at_acl[connection]['block']))
|
cmd.startswith(tuple(self.at_acl[connection]['block']))
|
||||||
|
|
||||||
async def send_at_cmd(self, at_cmd: str) -> None:
|
async def send_at_cmd(self, AT_cmd: str) -> None:
|
||||||
if self.state != State.up:
|
if self.state != State.up:
|
||||||
logger.warning(f'[{self.node_id}] ignore AT+ cmd,'
|
logger.warning(f'[{self.node_id}] ignore AT+ cmd,'
|
||||||
' as the state is not UP')
|
' as the state is not UP')
|
||||||
return
|
return
|
||||||
at_cmd = at_cmd.strip()
|
AT_cmd = AT_cmd.strip()
|
||||||
|
|
||||||
if self.at_cmd_forbidden(cmd=at_cmd, connection='mqtt'):
|
if self.at_cmd_forbidden(cmd=AT_cmd, connection='mqtt'):
|
||||||
data_json = f'\'{at_cmd}\' is forbidden'
|
data_json = f'\'{AT_cmd}\' is forbidden'
|
||||||
node_id = self.node_id
|
node_id = self.node_id
|
||||||
key = 'at_resp'
|
key = 'at_resp'
|
||||||
logger.info(f'{key}: {data_json}')
|
logger.info(f'{key}: {data_json}')
|
||||||
@@ -458,9 +453,9 @@ class SolarmanV5(Message):
|
|||||||
|
|
||||||
self.forward_at_cmd_resp = False
|
self.forward_at_cmd_resp = False
|
||||||
self.__build_header(0x4510)
|
self.__build_header(0x4510)
|
||||||
self._send_buffer += struct.pack(f'<BHLLL{len(at_cmd)}sc', self.AT_CMD,
|
self._send_buffer += struct.pack(f'<BHLLL{len(AT_cmd)}sc', self.AT_CMD,
|
||||||
0x0002, 0, 0, 0,
|
2, 0, 0, 0, AT_cmd.encode('utf-8'),
|
||||||
at_cmd.encode('utf-8'), b'\r')
|
b'\r')
|
||||||
self.__finish_send_msg()
|
self.__finish_send_msg()
|
||||||
try:
|
try:
|
||||||
await self.async_write('Send AT Command:')
|
await self.async_write('Send AT Command:')
|
||||||
@@ -472,19 +467,19 @@ class SolarmanV5(Message):
|
|||||||
|
|
||||||
def __build_model_name(self):
|
def __build_model_name(self):
|
||||||
db = self.db
|
db = self.db
|
||||||
max_pow = db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
MaxPow = db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
|
||||||
rated = db.get_db_value(Register.RATED_POWER, 0)
|
Rated = db.get_db_value(Register.RATED_POWER, 0)
|
||||||
model = None
|
Model = None
|
||||||
if max_pow == 2000:
|
if MaxPow == 2000:
|
||||||
if rated == 800 or rated == 600:
|
if Rated == 800 or Rated == 600:
|
||||||
model = f'TSOL-MS{max_pow}({rated})'
|
Model = f'TSOL-MS{MaxPow}({Rated})'
|
||||||
else:
|
else:
|
||||||
model = f'TSOL-MS{max_pow}'
|
Model = f'TSOL-MS{MaxPow}'
|
||||||
elif max_pow == 1800 or max_pow == 1600:
|
elif MaxPow == 1800 or MaxPow == 1600:
|
||||||
model = f'TSOL-MS{max_pow}'
|
Model = f'TSOL-MS{MaxPow}'
|
||||||
if model:
|
if Model:
|
||||||
logger.info(f'Model: {model}')
|
logger.info(f'Model: {Model}')
|
||||||
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, model)
|
self.db.set_db_def_value(Register.EQUIPMENT_MODEL, Model)
|
||||||
|
|
||||||
def __process_data(self, ftype, ts):
|
def __process_data(self, ftype, ts):
|
||||||
inv_update = False
|
inv_update = False
|
||||||
@@ -509,7 +504,7 @@ class SolarmanV5(Message):
|
|||||||
|
|
||||||
def msg_dev_ind(self):
|
def msg_dev_ind(self):
|
||||||
data = self._recv_buffer[self.header_len:]
|
data = self._recv_buffer[self.header_len:]
|
||||||
result = struct.unpack_from(self.HDR_FMT, data, 0)
|
result = struct.unpack_from('<BLLL', data, 0)
|
||||||
ftype = result[0] # always 2
|
ftype = result[0] # always 2
|
||||||
total = result[1]
|
total = result[1]
|
||||||
tim = result[2]
|
tim = result[2]
|
||||||
@@ -523,8 +518,6 @@ class SolarmanV5(Message):
|
|||||||
else:
|
else:
|
||||||
ts = None
|
ts = None
|
||||||
self.__process_data(ftype, ts)
|
self.__process_data(ftype, ts)
|
||||||
self.sensor_list = int(self.db.get_db_value(Register.SENSOR_LIST, 0),
|
|
||||||
16)
|
|
||||||
self.__forward_msg()
|
self.__forward_msg()
|
||||||
self.__send_ack_rsp(0x1110, ftype)
|
self.__send_ack_rsp(0x1110, ftype)
|
||||||
|
|
||||||
@@ -532,16 +525,12 @@ class SolarmanV5(Message):
|
|||||||
data = self._recv_buffer
|
data = self._recv_buffer
|
||||||
result = struct.unpack_from('<BHLLLHL', data, self.header_len)
|
result = struct.unpack_from('<BHLLLHL', data, self.header_len)
|
||||||
ftype = result[0] # 1 or 0x81
|
ftype = result[0] # 1 or 0x81
|
||||||
sensor = result[1]
|
|
||||||
total = result[2]
|
total = result[2]
|
||||||
tim = result[3]
|
tim = result[3]
|
||||||
if 1 == ftype:
|
if 1 == ftype:
|
||||||
self.time_ofs = result[4]
|
self.time_ofs = result[4]
|
||||||
unkn = result[5]
|
unkn = result[5]
|
||||||
cnt = result[6]
|
cnt = result[6]
|
||||||
if sensor != self.sensor_list:
|
|
||||||
logger.warning(f'Unexpected Sensor-List:{sensor:04x}'
|
|
||||||
f' (!={self.sensor_list:04x})')
|
|
||||||
logger.info(f'ftype:{ftype:02x} timer:{tim:08x}s'
|
logger.info(f'ftype:{ftype:02x} timer:{tim:08x}s'
|
||||||
f' ??: {unkn:04x} cnt:{cnt}')
|
f' ??: {unkn:04x} cnt:{cnt}')
|
||||||
if self.time_ofs:
|
if self.time_ofs:
|
||||||
@@ -558,7 +547,7 @@ class SolarmanV5(Message):
|
|||||||
|
|
||||||
def msg_sync_start(self):
|
def msg_sync_start(self):
|
||||||
data = self._recv_buffer[self.header_len:]
|
data = self._recv_buffer[self.header_len:]
|
||||||
result = struct.unpack_from(self.HDR_FMT, data, 0)
|
result = struct.unpack_from('<BLLL', data, 0)
|
||||||
ftype = result[0]
|
ftype = result[0]
|
||||||
total = result[1]
|
total = result[1]
|
||||||
self.time_ofs = result[3]
|
self.time_ofs = result[3]
|
||||||
@@ -575,17 +564,17 @@ class SolarmanV5(Message):
|
|||||||
result = struct.unpack_from('<B', data, 0)
|
result = struct.unpack_from('<B', data, 0)
|
||||||
ftype = result[0]
|
ftype = result[0]
|
||||||
if ftype == self.AT_CMD:
|
if ftype == self.AT_CMD:
|
||||||
at_cmd = data[15:].decode()
|
AT_cmd = data[15:].decode()
|
||||||
if self.at_cmd_forbidden(cmd=at_cmd, connection='tsun'):
|
if self.at_cmd_forbidden(cmd=AT_cmd, connection='tsun'):
|
||||||
self.inc_counter('AT_Command_Blocked')
|
self.inc_counter('AT_Command_Blocked')
|
||||||
return
|
return
|
||||||
self.inc_counter('AT_Command')
|
self.inc_counter('AT_Command')
|
||||||
self.forward_at_cmd_resp = True
|
self.forward_at_cmd_resp = True
|
||||||
|
|
||||||
elif ftype == self.MB_RTU_CMD:
|
elif ftype == self.MB_RTU_CMD:
|
||||||
if self.remote_stream.mb.recv_req(data[15:],
|
if self.remoteStream.mb.recv_req(data[15:],
|
||||||
self.remote_stream.
|
self.remoteStream.
|
||||||
__forward_msg):
|
__forward_msg):
|
||||||
self.inc_counter('Modbus_Command')
|
self.inc_counter('Modbus_Command')
|
||||||
else:
|
else:
|
||||||
logger.error('Invalid Modbus Msg')
|
logger.error('Invalid Modbus Msg')
|
||||||
@@ -604,9 +593,9 @@ class SolarmanV5(Message):
|
|||||||
if self.forward_at_cmd_resp:
|
if self.forward_at_cmd_resp:
|
||||||
return logging.INFO
|
return logging.INFO
|
||||||
return logging.DEBUG
|
return logging.DEBUG
|
||||||
elif ftype == self.MB_RTU_CMD \
|
elif ftype == self.MB_RTU_CMD:
|
||||||
and self.server_side:
|
if self.server_side:
|
||||||
return self.mb.last_log_lvl
|
return self.mb.last_log_lvl
|
||||||
|
|
||||||
return logging.WARNING
|
return logging.WARNING
|
||||||
|
|
||||||
@@ -623,30 +612,28 @@ class SolarmanV5(Message):
|
|||||||
self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
|
||||||
return
|
return
|
||||||
elif ftype == self.MB_RTU_CMD:
|
elif ftype == self.MB_RTU_CMD:
|
||||||
self.__modbus_command_rsp(data)
|
valid = data[1]
|
||||||
|
modbus_msg_len = self.data_len - 14
|
||||||
|
# logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}')
|
||||||
|
if valid == 1 and modbus_msg_len > 4:
|
||||||
|
# logger.info(f'first byte modbus:{data[14]}')
|
||||||
|
inv_update = False
|
||||||
|
self.modbus_elms = 0
|
||||||
|
|
||||||
|
for key, update, _ in self.mb.recv_resp(self.db, data[14:],
|
||||||
|
self.node_id):
|
||||||
|
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()
|
||||||
return
|
return
|
||||||
self.__forward_msg()
|
self.__forward_msg()
|
||||||
|
|
||||||
def __modbus_command_rsp(self, data):
|
|
||||||
'''precess MODBUS RTU response'''
|
|
||||||
valid = data[1]
|
|
||||||
modbus_msg_len = self.data_len - 14
|
|
||||||
# logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}')
|
|
||||||
if valid == 1 and modbus_msg_len > 4:
|
|
||||||
# logger.info(f'first byte modbus:{data[14]}')
|
|
||||||
inv_update = False
|
|
||||||
self.modbus_elms = 0
|
|
||||||
for key, update, _ in self.mb.recv_resp(self.db, data[14:],
|
|
||||||
self.node_id):
|
|
||||||
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()
|
|
||||||
|
|
||||||
def msg_hbeat_ind(self):
|
def msg_hbeat_ind(self):
|
||||||
data = self._recv_buffer[self.header_len:]
|
data = self._recv_buffer[self.header_len:]
|
||||||
result = struct.unpack_from('<B', data, 0)
|
result = struct.unpack_from('<B', data, 0)
|
||||||
@@ -658,7 +645,7 @@ class SolarmanV5(Message):
|
|||||||
|
|
||||||
def msg_sync_end(self):
|
def msg_sync_end(self):
|
||||||
data = self._recv_buffer[self.header_len:]
|
data = self._recv_buffer[self.header_len:]
|
||||||
result = struct.unpack_from(self.HDR_FMT, data, 0)
|
result = struct.unpack_from('<BLLL', data, 0)
|
||||||
ftype = result[0]
|
ftype = result[0]
|
||||||
total = result[1]
|
total = result[1]
|
||||||
self.time_ofs = result[3]
|
self.time_ofs = result[3]
|
||||||
|
|||||||
390
app/src/infos.py
390
app/src/infos.py
@@ -96,7 +96,6 @@ class Register(Enum):
|
|||||||
HEARTBEAT_INTERVAL = 406
|
HEARTBEAT_INTERVAL = 406
|
||||||
IP_ADDRESS = 407
|
IP_ADDRESS = 407
|
||||||
POLLING_INTERVAL = 408
|
POLLING_INTERVAL = 408
|
||||||
SENSOR_LIST = 409
|
|
||||||
EVENT_401 = 500
|
EVENT_401 = 500
|
||||||
EVENT_402 = 501
|
EVENT_402 = 501
|
||||||
EVENT_403 = 502
|
EVENT_403 = 502
|
||||||
@@ -131,16 +130,16 @@ class ClrAtMidnight:
|
|||||||
return
|
return
|
||||||
|
|
||||||
prfx += f'{keys[0]}'
|
prfx += f'{keys[0]}'
|
||||||
db_dict = cls.db
|
dict = cls.db
|
||||||
if prfx not in db_dict:
|
if prfx not in dict:
|
||||||
db_dict[prfx] = {}
|
dict[prfx] = {}
|
||||||
db_dict = db_dict[prfx]
|
dict = dict[prfx]
|
||||||
|
|
||||||
for key in keys[1:-1]:
|
for key in keys[1:-1]:
|
||||||
if key not in db_dict:
|
if key not in dict:
|
||||||
db_dict[key] = {}
|
dict[key] = {}
|
||||||
db_dict = db_dict[key]
|
dict = dict[key]
|
||||||
db_dict[keys[-1]] = 0
|
dict[keys[-1]] = 0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def elm(cls) -> Generator[tuple[str, dict], None, None]:
|
def elm(cls) -> Generator[tuple[str, dict], None, None]:
|
||||||
@@ -150,21 +149,10 @@ class ClrAtMidnight:
|
|||||||
|
|
||||||
|
|
||||||
class Infos:
|
class Infos:
|
||||||
LIGHTNING = 'mdi:lightning-bolt'
|
|
||||||
COUNTER = 'mdi:counter'
|
|
||||||
GAUGE = 'mdi:gauge'
|
|
||||||
SOLAR_POWER_VAR = 'mdi:solar-power-variant'
|
|
||||||
SOLAR_POWER = 'mdi:solar-power'
|
|
||||||
WIFI = 'mdi:wifi'
|
|
||||||
UPDATE = 'mdi:update'
|
|
||||||
DAILY_GEN = 'Daily Generation'
|
|
||||||
TOTAL_GEN = 'Total Generation'
|
|
||||||
FMT_INT = '| int'
|
|
||||||
FMT_FLOAT = '| float'
|
|
||||||
FMT_STRING_SEC = '| string + " s"'
|
|
||||||
stat = {}
|
stat = {}
|
||||||
app_name = os.getenv('SERVICE_NAME', 'proxy')
|
app_name = os.getenv('SERVICE_NAME', 'proxy')
|
||||||
version = os.getenv('VERSION', 'unknown')
|
version = os.getenv('VERSION', 'unknown')
|
||||||
|
|
||||||
new_stat_data = {}
|
new_stat_data = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -230,9 +218,9 @@ class Infos:
|
|||||||
Register.SERIAL_NUMBER: {'name': ['inverter', 'Serial_Number'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.SERIAL_NUMBER: {'name': ['inverter', 'Serial_Number'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
Register.EQUIPMENT_MODEL: {'name': ['inverter', 'Equipment_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EQUIPMENT_MODEL: {'name': ['inverter', 'Equipment_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
Register.NO_INPUTS: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.NO_INPUTS: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'val_tpl': __designed_power_val_tpl, 'name': 'Max Designed Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'val_tpl': __designed_power_val_tpl, 'name': 'Max Designed Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'val_tpl': __rated_power_val_tpl, 'name': 'Rated Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'val_tpl': __rated_power_val_tpl, 'name': 'Rated Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.OUTPUT_COEFFICIENT: {'name': ['inverter', 'Output_Coefficient'], 'level': logging.INFO, 'unit': '%', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'output_coef_', 'val_tpl': __output_coef_val_tpl, 'name': 'Output Coefficient', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.OUTPUT_COEFFICIENT: {'name': ['inverter', 'Output_Coefficient'], 'level': logging.INFO, 'unit': '%', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'output_coef_', 'val_tpl': __output_coef_val_tpl, 'name': 'Output Coefficient', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV1_MANUFACTURER: {'name': ['inverter', 'PV1_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.PV1_MANUFACTURER: {'name': ['inverter', 'PV1_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
Register.PV1_MODEL: {'name': ['inverter', 'PV1_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.PV1_MODEL: {'name': ['inverter', 'PV1_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
Register.PV2_MANUFACTURER: {'name': ['inverter', 'PV2_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.PV2_MANUFACTURER: {'name': ['inverter', 'PV2_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
@@ -247,19 +235,19 @@ class Infos:
|
|||||||
Register.PV6_MODEL: {'name': ['inverter', 'PV6_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.PV6_MODEL: {'name': ['inverter', 'PV6_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
|
|
||||||
# proxy:
|
# proxy:
|
||||||
Register.INVERTER_CNT: {'name': ['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_count_', 'fmt': FMT_INT, 'name': 'Active Inverter Connections', 'icon': COUNTER}}, # noqa: E501
|
Register.INVERTER_CNT: {'name': ['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_count_', 'fmt': '| int', 'name': 'Active Inverter Connections', 'icon': 'mdi:counter'}}, # noqa: E501
|
||||||
Register.UNKNOWN_SNR: {'name': ['proxy', 'Unknown_SNR'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_snr_', 'fmt': FMT_INT, 'name': 'Unknown Serial No', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.UNKNOWN_SNR: {'name': ['proxy', 'Unknown_SNR'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_snr_', 'fmt': '| int', 'name': 'Unknown Serial No', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.UNKNOWN_MSG: {'name': ['proxy', 'Unknown_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_msg_', 'fmt': FMT_INT, 'name': 'Unknown Msg Type', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.UNKNOWN_MSG: {'name': ['proxy', 'Unknown_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_msg_', 'fmt': '| int', 'name': 'Unknown Msg Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.INVALID_DATA_TYPE: {'name': ['proxy', 'Invalid_Data_Type'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_data_type_', 'fmt': FMT_INT, 'name': 'Invalid Data Type', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.INVALID_DATA_TYPE: {'name': ['proxy', 'Invalid_Data_Type'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_data_type_', 'fmt': '| int', 'name': 'Invalid Data Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.INTERNAL_ERROR: {'name': ['proxy', 'Internal_Error'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'intern_err_', 'fmt': FMT_INT, 'name': 'Internal Error', 'icon': COUNTER, 'ent_cat': 'diagnostic', 'en': False}}, # noqa: E501
|
Register.INTERNAL_ERROR: {'name': ['proxy', 'Internal_Error'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'intern_err_', 'fmt': '| int', 'name': 'Internal Error', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic', 'en': False}}, # noqa: E501
|
||||||
Register.UNKNOWN_CTRL: {'name': ['proxy', 'Unknown_Ctrl'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_ctrl_', 'fmt': FMT_INT, 'name': 'Unknown Control Type', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.UNKNOWN_CTRL: {'name': ['proxy', 'Unknown_Ctrl'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_ctrl_', 'fmt': '| int', 'name': 'Unknown Control Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.OTA_START_MSG: {'name': ['proxy', 'OTA_Start_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'ota_start_cmd_', 'fmt': FMT_INT, 'name': 'OTA Start Cmd', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.OTA_START_MSG: {'name': ['proxy', 'OTA_Start_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'ota_start_cmd_', 'fmt': '| int', 'name': 'OTA Start Cmd', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.SW_EXCEPTION: {'name': ['proxy', 'SW_Exception'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'sw_exception_', 'fmt': FMT_INT, 'name': 'Internal SW Exception', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.SW_EXCEPTION: {'name': ['proxy', 'SW_Exception'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'sw_exception_', 'fmt': '| int', 'name': 'Internal SW Exception', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.INVALID_MSG_FMT: {'name': ['proxy', 'Invalid_Msg_Format'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_msg_fmt_', 'fmt': FMT_INT, 'name': 'Invalid Message Format', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.INVALID_MSG_FMT: {'name': ['proxy', 'Invalid_Msg_Format'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_msg_fmt_', 'fmt': '| int', 'name': 'Invalid Message Format', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.AT_COMMAND: {'name': ['proxy', 'AT_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_', 'fmt': FMT_INT, 'name': 'AT Command', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.AT_COMMAND: {'name': ['proxy', 'AT_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_', 'fmt': '| int', 'name': 'AT Command', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.AT_COMMAND_BLOCKED: {'name': ['proxy', 'AT_Command_Blocked'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_blocked_', 'fmt': FMT_INT, 'name': 'AT Command Blocked', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.AT_COMMAND_BLOCKED: {'name': ['proxy', 'AT_Command_Blocked'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_blocked_', 'fmt': '| int', 'name': 'AT Command Blocked', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.MODBUS_COMMAND: {'name': ['proxy', 'Modbus_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'modbus_cmd_', 'fmt': FMT_INT, 'name': 'Modbus Command', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.MODBUS_COMMAND: {'name': ['proxy', 'Modbus_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'modbus_cmd_', 'fmt': '| int', 'name': 'Modbus Command', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
# 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':FMT_FLOAT,'name': 'Grid Voltage'}}, # noqa: E501
|
# 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':'| float','name': 'Grid Voltage'}}, # noqa: E501
|
||||||
|
|
||||||
# events
|
# events
|
||||||
Register.EVENT_401: {'name': ['events', '401_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
Register.EVENT_401: {'name': ['events', '401_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||||
@@ -281,61 +269,60 @@ class Infos:
|
|||||||
|
|
||||||
# grid measures:
|
# grid measures:
|
||||||
Register.TS_GRID: {'name': ['grid', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
Register.TS_GRID: {'name': ['grid', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||||
Register.GRID_VOLTAGE: {'name': ['grid', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'inverter', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'out_volt_', 'fmt': FMT_FLOAT, 'name': 'Grid Voltage', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.GRID_VOLTAGE: {'name': ['grid', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'inverter', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'out_volt_', 'fmt': '| float', 'name': 'Grid Voltage', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.GRID_CURRENT: {'name': ['grid', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'inverter', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'out_cur_', 'fmt': FMT_FLOAT, 'name': 'Grid Current', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.GRID_CURRENT: {'name': ['grid', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'inverter', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'out_cur_', 'fmt': '| float', 'name': 'Grid Current', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.GRID_FREQUENCY: {'name': ['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha': {'dev': 'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id': 'out_freq_', 'fmt': FMT_FLOAT, 'name': 'Grid Frequency', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.GRID_FREQUENCY: {'name': ['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha': {'dev': 'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id': 'out_freq_', 'fmt': '| float', 'name': 'Grid Frequency', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.OUTPUT_POWER: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': FMT_FLOAT, 'name': 'Power'}}, # noqa: E501
|
Register.OUTPUT_POWER: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': '| float', 'name': 'Power'}}, # noqa: E501
|
||||||
Register.INVERTER_TEMP: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': FMT_INT, 'name': 'Temperature'}}, # noqa: E501
|
Register.INVERTER_TEMP: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': '| int', 'name': 'Temperature'}}, # noqa: E501
|
||||||
Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': 'mdi:power'}}, # noqa: E501
|
Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': 'mdi:power'}}, # noqa: E501
|
||||||
|
|
||||||
# input measures:
|
# input measures:
|
||||||
Register.TS_INPUT: {'name': ['input', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
Register.TS_INPUT: {'name': ['input', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||||
Register.PV1_VOLTAGE: {'name': ['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv1_', 'val_tpl': "{{ (value_json['pv1']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV1_VOLTAGE: {'name': ['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv1_', 'val_tpl': "{{ (value_json['pv1']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV1_CURRENT: {'name': ['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv1_', 'val_tpl': "{{ (value_json['pv1']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV1_CURRENT: {'name': ['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv1_', 'val_tpl': "{{ (value_json['pv1']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV1_POWER: {'name': ['input', 'pv1', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv1_', 'val_tpl': "{{ (value_json['pv1']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV1_POWER: {'name': ['input', 'pv1', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv1_', 'val_tpl': "{{ (value_json['pv1']['Power'] | float)}}"}}, # noqa: E501
|
||||||
Register.PV2_VOLTAGE: {'name': ['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv2_', 'val_tpl': "{{ (value_json['pv2']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV2_VOLTAGE: {'name': ['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv2_', 'val_tpl': "{{ (value_json['pv2']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV2_CURRENT: {'name': ['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv2_', 'val_tpl': "{{ (value_json['pv2']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV2_CURRENT: {'name': ['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv2_', 'val_tpl': "{{ (value_json['pv2']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV2_POWER: {'name': ['input', 'pv2', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv2_', 'val_tpl': "{{ (value_json['pv2']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV2_POWER: {'name': ['input', 'pv2', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv2_', 'val_tpl': "{{ (value_json['pv2']['Power'] | float)}}"}}, # noqa: E501
|
||||||
Register.PV3_VOLTAGE: {'name': ['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv3_', 'val_tpl': "{{ (value_json['pv3']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV3_VOLTAGE: {'name': ['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv3_', 'val_tpl': "{{ (value_json['pv3']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV3_CURRENT: {'name': ['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv3_', 'val_tpl': "{{ (value_json['pv3']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV3_CURRENT: {'name': ['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv3_', 'val_tpl': "{{ (value_json['pv3']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV3_POWER: {'name': ['input', 'pv3', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv3_', 'val_tpl': "{{ (value_json['pv3']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV3_POWER: {'name': ['input', 'pv3', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv3_', 'val_tpl': "{{ (value_json['pv3']['Power'] | float)}}"}}, # noqa: E501
|
||||||
Register.PV4_VOLTAGE: {'name': ['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv4_', 'val_tpl': "{{ (value_json['pv4']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV4_VOLTAGE: {'name': ['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv4_', 'val_tpl': "{{ (value_json['pv4']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV4_CURRENT: {'name': ['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv4_', 'val_tpl': "{{ (value_json['pv4']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV4_CURRENT: {'name': ['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv4_', 'val_tpl': "{{ (value_json['pv4']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV4_POWER: {'name': ['input', 'pv4', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv4_', 'val_tpl': "{{ (value_json['pv4']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV4_POWER: {'name': ['input', 'pv4', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv4_', 'val_tpl': "{{ (value_json['pv4']['Power'] | float)}}"}}, # noqa: E501
|
||||||
Register.PV5_VOLTAGE: {'name': ['input', 'pv5', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv5', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv5_', 'val_tpl': "{{ (value_json['pv5']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV5_VOLTAGE: {'name': ['input', 'pv5', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv5', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv5_', 'val_tpl': "{{ (value_json['pv5']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV5_CURRENT: {'name': ['input', 'pv5', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv5', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv5_', 'val_tpl': "{{ (value_json['pv5']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV5_CURRENT: {'name': ['input', 'pv5', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv5', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv5_', 'val_tpl': "{{ (value_json['pv5']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV5_POWER: {'name': ['input', 'pv5', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv5', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv5_', 'val_tpl': "{{ (value_json['pv5']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV5_POWER: {'name': ['input', 'pv5', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv5', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv5_', 'val_tpl': "{{ (value_json['pv5']['Power'] | float)}}"}}, # noqa: E501
|
||||||
Register.PV6_VOLTAGE: {'name': ['input', 'pv6', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv6', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv6_', 'val_tpl': "{{ (value_json['pv6']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV6_VOLTAGE: {'name': ['input', 'pv6', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv6', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv6_', 'val_tpl': "{{ (value_json['pv6']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV6_CURRENT: {'name': ['input', 'pv6', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv6', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv6_', 'val_tpl': "{{ (value_json['pv6']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.PV6_CURRENT: {'name': ['input', 'pv6', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv6', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv6_', 'val_tpl': "{{ (value_json['pv6']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.PV6_POWER: {'name': ['input', 'pv6', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv6', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv6_', 'val_tpl': "{{ (value_json['pv6']['Power'] | float)}}"}}, # noqa: E501
|
Register.PV6_POWER: {'name': ['input', 'pv6', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv6', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv6_', 'val_tpl': "{{ (value_json['pv6']['Power'] | float)}}"}}, # noqa: E501
|
||||||
Register.PV1_DAILY_GENERATION: {'name': ['input', 'pv1', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv1_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv1']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
|
Register.PV1_DAILY_GENERATION: {'name': ['input', 'pv1', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv1_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv1']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||||
Register.PV1_TOTAL_GENERATION: {'name': ['input', 'pv1', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv1_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv1']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
Register.PV1_TOTAL_GENERATION: {'name': ['input', 'pv1', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv1_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv1']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||||
Register.PV2_DAILY_GENERATION: {'name': ['input', 'pv2', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv2_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv2']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
|
Register.PV2_DAILY_GENERATION: {'name': ['input', 'pv2', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv2_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv2']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||||
Register.PV2_TOTAL_GENERATION: {'name': ['input', 'pv2', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv2_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv2']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
Register.PV2_TOTAL_GENERATION: {'name': ['input', 'pv2', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv2_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv2']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||||
Register.PV3_DAILY_GENERATION: {'name': ['input', 'pv3', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv3_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv3']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
|
Register.PV3_DAILY_GENERATION: {'name': ['input', 'pv3', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv3_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv3']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||||
Register.PV3_TOTAL_GENERATION: {'name': ['input', 'pv3', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv3_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv3']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
Register.PV3_TOTAL_GENERATION: {'name': ['input', 'pv3', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv3_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv3']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||||
Register.PV4_DAILY_GENERATION: {'name': ['input', 'pv4', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv4_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv4']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
|
Register.PV4_DAILY_GENERATION: {'name': ['input', 'pv4', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv4_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv4']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||||
Register.PV4_TOTAL_GENERATION: {'name': ['input', 'pv4', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv4_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv4']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
Register.PV4_TOTAL_GENERATION: {'name': ['input', 'pv4', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv4_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv4']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||||
Register.PV5_DAILY_GENERATION: {'name': ['input', 'pv5', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv5', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv5_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv5']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
|
Register.PV5_DAILY_GENERATION: {'name': ['input', 'pv5', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv5', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv5_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv5']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||||
Register.PV5_TOTAL_GENERATION: {'name': ['input', 'pv5', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv5', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv5_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv5']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
Register.PV5_TOTAL_GENERATION: {'name': ['input', 'pv5', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv5', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv5_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv5']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||||
Register.PV6_DAILY_GENERATION: {'name': ['input', 'pv6', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv6', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv6_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv6']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
|
Register.PV6_DAILY_GENERATION: {'name': ['input', 'pv6', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv6', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv6_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv6']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||||
Register.PV6_TOTAL_GENERATION: {'name': ['input', 'pv6', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv6', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv6_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv6']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
Register.PV6_TOTAL_GENERATION: {'name': ['input', 'pv6', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv6', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv6_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv6']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||||
# total:
|
# total:
|
||||||
Register.TS_TOTAL: {'name': ['total', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
Register.TS_TOTAL: {'name': ['total', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
||||||
Register.DAILY_GENERATION: {'name': ['total', 'Daily_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_', 'fmt': FMT_FLOAT, 'name': DAILY_GEN, 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
|
Register.DAILY_GENERATION: {'name': ['total', 'Daily_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_', 'fmt': '| float', 'name': 'Daily Generation', 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501
|
||||||
Register.TOTAL_GENERATION: {'name': ['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': FMT_FLOAT, 'name': TOTAL_GEN, 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
|
Register.TOTAL_GENERATION: {'name': ['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': '| float', 'name': 'Total Generation', 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
|
||||||
|
|
||||||
# controller:
|
# controller:
|
||||||
Register.SIGNAL_STRENGTH: {'name': ['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': FMT_INT, 'name': 'Signal Strength', 'icon': WIFI}}, # noqa: E501
|
Register.SIGNAL_STRENGTH: {'name': ['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': '| int', 'name': 'Signal Strength', 'icon': 'mdi:wifi'}}, # noqa: E501
|
||||||
Register.POWER_ON_TIME: {'name': ['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': FMT_INT, 'name': 'Power on Time', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.POWER_ON_TIME: {'name': ['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': '| int', 'name': 'Power on Time', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.COLLECT_INTERVAL: {'name': ['controller', 'Collect_Interval'], 'level': logging.DEBUG, 'unit': 'min', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " min"', 'name': 'Data Collect Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.COLLECT_INTERVAL: {'name': ['controller', 'Collect_Interval'], 'level': logging.DEBUG, 'unit': 'min', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " min"', 'name': 'Data Collect Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.CONNECT_COUNT: {'name': ['controller', 'Connect_Count'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': FMT_INT, 'name': 'Connect Count', 'icon': COUNTER, 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.CONNECT_COUNT: {'name': ['controller', 'Connect_Count'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': '| int', 'name': 'Connect Count', 'icon': 'mdi:counter', 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.COMMUNICATION_TYPE: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'comp': 'sensor', 'icon': WIFI}}, # noqa: E501
|
Register.COMMUNICATION_TYPE: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'comp': 'sensor', 'icon': 'mdi:wifi'}}, # noqa: E501
|
||||||
Register.DATA_UP_INTERVAL: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Data Up Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.DATA_UP_INTERVAL: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': '| string + " s"', 'name': 'Data Up Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.HEARTBEAT_INTERVAL: {'name': ['controller', 'Heartbeat_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'heartbeat_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Heartbeat Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.HEARTBEAT_INTERVAL: {'name': ['controller', 'Heartbeat_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'heartbeat_intval_', 'fmt': '| string + " s"', 'name': 'Heartbeat Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.IP_ADDRESS: {'name': ['controller', 'IP_Address'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'ip_address_', 'fmt': '| string', 'name': 'IP Address', 'icon': WIFI, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.IP_ADDRESS: {'name': ['controller', 'IP_Address'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'ip_address_', 'fmt': '| string', 'name': 'IP Address', 'icon': 'mdi:wifi', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.POLLING_INTERVAL: {'name': ['controller', 'Polling_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'polling_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Polling Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
|
Register.POLLING_INTERVAL: {'name': ['controller', 'Polling_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'polling_intval_', 'fmt': '| string + " s"', 'name': 'Polling Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||||
Register.SENSOR_LIST: {'name': ['controller', 'Sensor_List'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -346,7 +333,7 @@ class Infos:
|
|||||||
def info_defs(self) -> dict:
|
def info_defs(self) -> dict:
|
||||||
return self.__info_defs
|
return self.__info_defs
|
||||||
|
|
||||||
def dev_value(self, idx: str | int) -> str | int | float | dict | None:
|
def dev_value(self, idx: str | int) -> str | int | float | None:
|
||||||
'''returns the stored device value from our database
|
'''returns the stored device value from our database
|
||||||
|
|
||||||
idx:int ==> lookup the value in the database and return it as str,
|
idx:int ==> lookup the value in the database and return it as str,
|
||||||
@@ -359,29 +346,29 @@ class Infos:
|
|||||||
elif idx in self.info_defs:
|
elif idx in self.info_defs:
|
||||||
row = self.info_defs[idx]
|
row = self.info_defs[idx]
|
||||||
if 'singleton' in row and row['singleton']:
|
if 'singleton' in row and row['singleton']:
|
||||||
db_dict = self.stat
|
dict = self.stat
|
||||||
else:
|
else:
|
||||||
db_dict = self.db
|
dict = self.db
|
||||||
|
|
||||||
keys = row['name']
|
keys = row['name']
|
||||||
|
|
||||||
for key in keys:
|
for key in keys:
|
||||||
if key not in db_dict:
|
if key not in dict:
|
||||||
return None # value not found in the database
|
return None # value not found in the database
|
||||||
db_dict = db_dict[key]
|
dict = dict[key]
|
||||||
return db_dict # value of the reqeusted entry
|
return dict # value of the reqeusted entry
|
||||||
|
|
||||||
return None # unknwon idx, not in info_defs
|
return None # unknwon idx, not in info_defs
|
||||||
|
|
||||||
def inc_counter(self, counter: str) -> None:
|
def inc_counter(self, counter: str) -> None:
|
||||||
'''inc proxy statistic counter'''
|
'''inc proxy statistic counter'''
|
||||||
db_dict = self.stat['proxy']
|
dict = self.stat['proxy']
|
||||||
db_dict[counter] += 1
|
dict[counter] += 1
|
||||||
|
|
||||||
def dec_counter(self, counter: str) -> None:
|
def dec_counter(self, counter: str) -> None:
|
||||||
'''dec proxy statistic counter'''
|
'''dec proxy statistic counter'''
|
||||||
db_dict = self.stat['proxy']
|
dict = self.stat['proxy']
|
||||||
db_dict[counter] -= 1
|
dict[counter] -= 1
|
||||||
|
|
||||||
def ha_proxy_confs(self, ha_prfx: str, node_id: str, snr: str) \
|
def ha_proxy_confs(self, ha_prfx: str, node_id: str, snr: str) \
|
||||||
-> Generator[tuple[str, str, str, str], None, None]:
|
-> Generator[tuple[str, str, str, str], None, None]:
|
||||||
@@ -427,119 +414,99 @@ class Infos:
|
|||||||
return None
|
return None
|
||||||
elif singleton:
|
elif singleton:
|
||||||
return None
|
return None
|
||||||
|
prfx = ha_prfx + node_id
|
||||||
|
|
||||||
# check if we have details for home assistant
|
# check if we have details for home assistant
|
||||||
if 'ha' in row:
|
if 'ha' in row:
|
||||||
return self.__ha_conf(row, key, ha_prfx, node_id, snr, sug_area)
|
ha = row['ha']
|
||||||
return None
|
if 'comp' in ha:
|
||||||
|
component = ha['comp']
|
||||||
def __ha_conf(self, row, key, ha_prfx, node_id, snr,
|
else:
|
||||||
sug_area: str) -> tuple[str, str, str, str] | None:
|
component = 'sensor'
|
||||||
ha = row['ha']
|
attr = {}
|
||||||
if 'comp' in ha:
|
if 'name' in ha:
|
||||||
component = ha['comp']
|
attr['name'] = ha['name']
|
||||||
else:
|
else:
|
||||||
component = 'sensor'
|
attr['name'] = row['name'][-1]
|
||||||
attr = self.__build_attr(row, key, ha_prfx, node_id, snr)
|
attr['stat_t'] = prfx + row['name'][0]
|
||||||
if 'dev' in ha:
|
attr['dev_cla'] = ha['dev_cla']
|
||||||
device = self.info_devs[ha['dev']]
|
attr['stat_cla'] = ha['stat_cla']
|
||||||
if 'dep' in device and self.ignore_this_device(device['dep']): # noqa: E501
|
attr['uniq_id'] = ha['id']+snr
|
||||||
return None
|
if 'val_tpl' in ha:
|
||||||
attr['dev'] = self.__build_dev(device, key, ha, snr,
|
attr['val_tpl'] = ha['val_tpl']
|
||||||
sug_area)
|
elif 'fmt' in ha:
|
||||||
attr['o'] = self.__build_origin()
|
attr['val_tpl'] = '{{value_json' + f"['{row['name'][-1]}'] {ha['fmt']}" + '}}' # eg. 'val_tpl': "{{ value_json['Output_Power']|float }} # noqa: E501
|
||||||
|
|
||||||
else:
|
|
||||||
self.inc_counter('Internal_Error')
|
|
||||||
logging.error(f"Infos.info_defs: the row for {key} "
|
|
||||||
"missing 'dev' value for ha register")
|
|
||||||
return json.dumps(attr), component, node_id, attr['uniq_id']
|
|
||||||
|
|
||||||
def __build_attr(self, row, key, ha_prfx, node_id, snr):
|
|
||||||
attr = {}
|
|
||||||
ha = row['ha']
|
|
||||||
if 'name' in ha:
|
|
||||||
attr['name'] = ha['name']
|
|
||||||
else:
|
|
||||||
attr['name'] = row['name'][-1]
|
|
||||||
prfx = ha_prfx + node_id
|
|
||||||
attr['stat_t'] = prfx + row['name'][0]
|
|
||||||
attr['dev_cla'] = ha['dev_cla']
|
|
||||||
attr['stat_cla'] = ha['stat_cla']
|
|
||||||
attr['uniq_id'] = ha['id']+snr
|
|
||||||
if 'val_tpl' in ha:
|
|
||||||
attr['val_tpl'] = ha['val_tpl']
|
|
||||||
elif 'fmt' in ha:
|
|
||||||
attr['val_tpl'] = '{{value_json' + f"['{row['name'][-1]}'] {ha['fmt']}" + '}}' # eg. 'val_tpl': "{{ value_json['Output_Power']|float }} # noqa: E501
|
|
||||||
else:
|
|
||||||
self.inc_counter('Internal_Error')
|
|
||||||
logging.error(f"Infos.info_defs: the row for {key} do"
|
|
||||||
" not have a 'val_tpl' nor a 'fmt' value")
|
|
||||||
# add unit_of_meas only, if status_class isn't none. If
|
|
||||||
# status_cla is None we want a number format and not line
|
|
||||||
# graph in home assistant. A unit will change the number
|
|
||||||
# format to a line graph
|
|
||||||
if 'unit' in row and attr['stat_cla'] is not None:
|
|
||||||
attr['unit_of_meas'] = row['unit'] # 'unit_of_meas'
|
|
||||||
if 'icon' in ha:
|
|
||||||
attr['ic'] = ha['icon'] # icon for the entity
|
|
||||||
if 'nat_prc' in ha: # pragma: no cover
|
|
||||||
attr['sug_dsp_prc'] = ha['nat_prc'] # precison of floats
|
|
||||||
if 'ent_cat' in ha:
|
|
||||||
attr['ent_cat'] = ha['ent_cat'] # diagnostic, config
|
|
||||||
# enabled_by_default is deactivated, since it avoid the via
|
|
||||||
# setup of the devices. It seems, that there is a bug in home
|
|
||||||
# assistant. tested with 'Home Assistant 2023.10.4'
|
|
||||||
# if 'en' in ha: # enabled_by_default
|
|
||||||
# attr['en'] = ha['en']
|
|
||||||
return attr
|
|
||||||
|
|
||||||
def __build_dev(self, device, key, ha, snr, sug_area):
|
|
||||||
dev = {}
|
|
||||||
singleton = 'singleton' in device and device['singleton']
|
|
||||||
# the same name for 'name' and 'suggested area', so we get
|
|
||||||
# dedicated devices in home assistant with short value
|
|
||||||
# name and headline
|
|
||||||
if (sug_area == '' or singleton):
|
|
||||||
dev['name'] = device['name']
|
|
||||||
dev['sa'] = device['name']
|
|
||||||
else:
|
|
||||||
dev['name'] = device['name']+' - '+sug_area
|
|
||||||
dev['sa'] = device['name']+' - '+sug_area
|
|
||||||
self.__add_via_dev(dev, device, key, snr)
|
|
||||||
for key in ('mdl', 'mf', 'sw', 'hw'): # add optional
|
|
||||||
# values fpr 'modell', 'manufacturer', 'sw version' and
|
|
||||||
# 'hw version'
|
|
||||||
if key in device:
|
|
||||||
data = self.dev_value(device[key])
|
|
||||||
if data is not None:
|
|
||||||
dev[key] = data
|
|
||||||
if singleton:
|
|
||||||
dev['ids'] = [f"{ha['dev']}"]
|
|
||||||
else:
|
|
||||||
dev['ids'] = [f"{ha['dev']}_{snr}"]
|
|
||||||
return dev
|
|
||||||
|
|
||||||
def __add_via_dev(self, dev, device, key, snr):
|
|
||||||
if 'via' in device: # add the link to the parent device
|
|
||||||
via = device['via']
|
|
||||||
if via in self.info_devs:
|
|
||||||
via_dev = self.info_devs[via]
|
|
||||||
if 'singleton' in via_dev and via_dev['singleton']:
|
|
||||||
dev['via_device'] = via
|
|
||||||
else:
|
|
||||||
dev['via_device'] = f"{via}_{snr}"
|
|
||||||
else:
|
else:
|
||||||
self.inc_counter('Internal_Error')
|
self.inc_counter('Internal_Error')
|
||||||
logging.error(f"Infos.info_defs: the row for "
|
logging.error(f"Infos.info_defs: the row for {key} do"
|
||||||
f"{key} has an invalid via value: "
|
" not have a 'val_tpl' nor a 'fmt' value")
|
||||||
f"{via}")
|
# add unit_of_meas only, if status_class isn't none. If
|
||||||
|
# status_cla is None we want a number format and not line
|
||||||
def __build_origin(self):
|
# graph in home assistant. A unit will change the number
|
||||||
origin = {}
|
# format to a line graph
|
||||||
origin['name'] = self.app_name
|
if 'unit' in row and attr['stat_cla'] is not None:
|
||||||
origin['sw'] = self.version
|
attr['unit_of_meas'] = row['unit'] # 'unit_of_meas'
|
||||||
return origin
|
if 'icon' in ha:
|
||||||
|
attr['ic'] = ha['icon'] # icon for the entity
|
||||||
|
if 'nat_prc' in ha: # pragma: no cover
|
||||||
|
attr['sug_dsp_prc'] = ha['nat_prc'] # precison of floats
|
||||||
|
if 'ent_cat' in ha:
|
||||||
|
attr['ent_cat'] = ha['ent_cat'] # diagnostic, config
|
||||||
|
# enabled_by_default is deactivated, since it avoid the via
|
||||||
|
# setup of the devices. It seems, that there is a bug in home
|
||||||
|
# assistant. tested with 'Home Assistant 2023.10.4'
|
||||||
|
# if 'en' in ha: # enabled_by_default
|
||||||
|
# attr['en'] = ha['en']
|
||||||
|
if 'dev' in ha:
|
||||||
|
device = self.info_devs[ha['dev']]
|
||||||
|
if 'dep' in device and self.ignore_this_device(device['dep']): # noqa: E501
|
||||||
|
return None
|
||||||
|
dev = {}
|
||||||
|
# the same name for 'name' and 'suggested area', so we get
|
||||||
|
# dedicated devices in home assistant with short value
|
||||||
|
# name and headline
|
||||||
|
if (sug_area == '' or
|
||||||
|
('singleton' in device and device['singleton'])):
|
||||||
|
dev['name'] = device['name']
|
||||||
|
dev['sa'] = device['name']
|
||||||
|
else:
|
||||||
|
dev['name'] = device['name']+' - '+sug_area
|
||||||
|
dev['sa'] = device['name']+' - '+sug_area
|
||||||
|
if 'via' in device: # add the link to the parent device
|
||||||
|
via = device['via']
|
||||||
|
if via in self.info_devs:
|
||||||
|
via_dev = self.info_devs[via]
|
||||||
|
if 'singleton' in via_dev and via_dev['singleton']:
|
||||||
|
dev['via_device'] = via
|
||||||
|
else:
|
||||||
|
dev['via_device'] = f"{via}_{snr}"
|
||||||
|
else:
|
||||||
|
self.inc_counter('Internal_Error')
|
||||||
|
logging.error(f"Infos.info_defs: the row for "
|
||||||
|
f"{key} has an invalid via value: "
|
||||||
|
f"{via}")
|
||||||
|
for key in ('mdl', 'mf', 'sw', 'hw'): # add optional
|
||||||
|
# values fpr 'modell', 'manufacturer', 'sw version' and
|
||||||
|
# 'hw version'
|
||||||
|
if key in device:
|
||||||
|
data = self.dev_value(device[key])
|
||||||
|
if data is not None:
|
||||||
|
dev[key] = data
|
||||||
|
if 'singleton' in device and device['singleton']:
|
||||||
|
dev['ids'] = [f"{ha['dev']}"]
|
||||||
|
else:
|
||||||
|
dev['ids'] = [f"{ha['dev']}_{snr}"]
|
||||||
|
attr['dev'] = dev
|
||||||
|
origin = {}
|
||||||
|
origin['name'] = self.app_name
|
||||||
|
origin['sw'] = self.version
|
||||||
|
attr['o'] = origin
|
||||||
|
else:
|
||||||
|
self.inc_counter('Internal_Error')
|
||||||
|
logging.error(f"Infos.info_defs: the row for {key} "
|
||||||
|
"missing 'dev' value for ha register")
|
||||||
|
return json.dumps(attr), component, node_id, attr['uniq_id']
|
||||||
|
return None
|
||||||
|
|
||||||
def ha_remove(self, key, node_id, snr) -> tuple[str, str, str, str] | None:
|
def ha_remove(self, key, node_id, snr) -> tuple[str, str, str, str] | None:
|
||||||
'''Method to build json unregister struct for home-assistant
|
'''Method to build json unregister struct for home-assistant
|
||||||
@@ -558,8 +525,9 @@ class Infos:
|
|||||||
return None
|
return None
|
||||||
row = self.info_defs[key]
|
row = self.info_defs[key]
|
||||||
|
|
||||||
if 'singleton' in row and row['singleton']:
|
if 'singleton' in row:
|
||||||
return None
|
if row['singleton']:
|
||||||
|
return None
|
||||||
|
|
||||||
# check if we have details for home assistant
|
# check if we have details for home assistant
|
||||||
if 'ha' in row:
|
if 'ha' in row:
|
||||||
@@ -574,7 +542,7 @@ class Infos:
|
|||||||
return json.dumps(attr), component, node_id, uniq_id
|
return json.dumps(attr), component, node_id, uniq_id
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _key_obj(self, id: Register) -> tuple:
|
def _key_obj(self, id: Register) -> list:
|
||||||
d = self.info_defs.get(id, {'name': None, 'level': logging.DEBUG,
|
d = self.info_defs.get(id, {'name': None, 'level': logging.DEBUG,
|
||||||
'unit': ''})
|
'unit': ''})
|
||||||
if 'ha' in d and 'must_incr' in d['ha']:
|
if 'ha' in d and 'must_incr' in d['ha']:
|
||||||
@@ -586,21 +554,21 @@ class Infos:
|
|||||||
|
|
||||||
def update_db(self, keys: list, must_incr: bool, result):
|
def update_db(self, keys: list, must_incr: bool, result):
|
||||||
name = ''
|
name = ''
|
||||||
db_dict = self.db
|
dict = self.db
|
||||||
for key in keys[:-1]:
|
for key in keys[:-1]:
|
||||||
if key not in db_dict:
|
if key not in dict:
|
||||||
db_dict[key] = {}
|
dict[key] = {}
|
||||||
db_dict = db_dict[key]
|
dict = dict[key]
|
||||||
name += key + '.'
|
name += key + '.'
|
||||||
if keys[-1] not in db_dict:
|
if keys[-1] not in dict:
|
||||||
update = (not must_incr or result > 0)
|
update = (not must_incr or result > 0)
|
||||||
else:
|
else:
|
||||||
if must_incr:
|
if must_incr:
|
||||||
update = db_dict[keys[-1]] < result
|
update = dict[keys[-1]] < result
|
||||||
else:
|
else:
|
||||||
update = db_dict[keys[-1]] != result
|
update = dict[keys[-1]] != result
|
||||||
if update:
|
if update:
|
||||||
db_dict[keys[-1]] = result
|
dict[keys[-1]] = result
|
||||||
name += keys[-1]
|
name += keys[-1]
|
||||||
return name, update
|
return name, update
|
||||||
|
|
||||||
@@ -654,13 +622,13 @@ class Infos:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
if 'gte' in dep:
|
if 'gte' in dep:
|
||||||
return value < dep['gte']
|
return not value >= dep['gte']
|
||||||
elif 'less_eq' in dep:
|
elif 'less_eq' in dep:
|
||||||
return value > dep['less_eq']
|
return not value <= dep['less_eq']
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def set_pv_module_details(self, inv: dict) -> None:
|
def set_pv_module_details(self, inv: dict) -> None:
|
||||||
pvs = {'pv1': {'manufacturer': Register.PV1_MANUFACTURER, 'model': Register.PV1_MODEL}, # noqa: E501
|
map = {'pv1': {'manufacturer': Register.PV1_MANUFACTURER, 'model': Register.PV1_MODEL}, # noqa: E501
|
||||||
'pv2': {'manufacturer': Register.PV2_MANUFACTURER, 'model': Register.PV2_MODEL}, # noqa: E501
|
'pv2': {'manufacturer': Register.PV2_MANUFACTURER, 'model': Register.PV2_MODEL}, # noqa: E501
|
||||||
'pv3': {'manufacturer': Register.PV3_MANUFACTURER, 'model': Register.PV3_MODEL}, # noqa: E501
|
'pv3': {'manufacturer': Register.PV3_MANUFACTURER, 'model': Register.PV3_MODEL}, # noqa: E501
|
||||||
'pv4': {'manufacturer': Register.PV4_MANUFACTURER, 'model': Register.PV4_MODEL}, # noqa: E501
|
'pv4': {'manufacturer': Register.PV4_MANUFACTURER, 'model': Register.PV4_MODEL}, # noqa: E501
|
||||||
@@ -668,7 +636,7 @@ class Infos:
|
|||||||
'pv6': {'manufacturer': Register.PV6_MANUFACTURER, 'model': Register.PV6_MODEL} # noqa: E501
|
'pv6': {'manufacturer': Register.PV6_MANUFACTURER, 'model': Register.PV6_MODEL} # noqa: E501
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, reg in pvs.items():
|
for key, reg in map.items():
|
||||||
if key in inv:
|
if key in inv:
|
||||||
if 'manufacturer' in inv[key]:
|
if 'manufacturer' in inv[key]:
|
||||||
self.set_db_def_value(reg['manufacturer'],
|
self.set_db_def_value(reg['manufacturer'],
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
if __name__ == "app.src.inverter":
|
from config import Config
|
||||||
from app.src.config import Config
|
from mqtt import Mqtt
|
||||||
from app.src.mqtt import Mqtt
|
from infos import Infos
|
||||||
from app.src.infos import Infos
|
|
||||||
else: # pragma: no cover
|
|
||||||
from config import Config
|
|
||||||
from mqtt import Mqtt
|
|
||||||
from infos import Infos
|
|
||||||
|
|
||||||
|
# logger = logging.getLogger('conn')
|
||||||
logger_mqtt = logging.getLogger('mqtt')
|
logger_mqtt = logging.getLogger('mqtt')
|
||||||
|
|
||||||
|
|
||||||
@@ -76,7 +72,7 @@ class Inverter():
|
|||||||
Infos.new_stat_data[key] = False
|
Infos.new_stat_data[key] = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def class_close(cls, loop) -> None: # pragma: no cover
|
def class_close(cls, loop) -> None:
|
||||||
logging.debug('Inverter.class_close')
|
logging.debug('Inverter.class_close')
|
||||||
logging.info('Close MQTT Task')
|
logging.info('Close MQTT Task')
|
||||||
loop.run_until_complete(cls.mqtt.close())
|
loop.run_until_complete(cls.mqtt.close())
|
||||||
|
|||||||
@@ -14,25 +14,6 @@ else: # pragma: no cover
|
|||||||
logger = logging.getLogger('msg')
|
logger = logging.getLogger('msg')
|
||||||
|
|
||||||
|
|
||||||
def __hex_val(n, data, data_len):
|
|
||||||
line = ''
|
|
||||||
for j in range(n-16, n):
|
|
||||||
if j >= data_len:
|
|
||||||
break
|
|
||||||
line += '%02x ' % abs(data[j])
|
|
||||||
return line
|
|
||||||
|
|
||||||
|
|
||||||
def __asc_val(n, data, data_len):
|
|
||||||
line = ''
|
|
||||||
for j in range(n-16, n):
|
|
||||||
if j >= data_len:
|
|
||||||
break
|
|
||||||
c = data[j] if not (data[j] < 0x20 or data[j] > 0x7e) else '.'
|
|
||||||
line += '%c' % c
|
|
||||||
return line
|
|
||||||
|
|
||||||
|
|
||||||
def hex_dump_memory(level, info, data, data_len):
|
def hex_dump_memory(level, info, data, data_len):
|
||||||
n = 0
|
n = 0
|
||||||
lines = []
|
lines = []
|
||||||
@@ -45,9 +26,20 @@ def hex_dump_memory(level, info, data, data_len):
|
|||||||
line = ' '
|
line = ' '
|
||||||
line += '%04x | ' % (i)
|
line += '%04x | ' % (i)
|
||||||
n += 16
|
n += 16
|
||||||
line += __hex_val(n, data, data_len)
|
|
||||||
|
for j in range(n-16, n):
|
||||||
|
if j >= data_len:
|
||||||
|
break
|
||||||
|
line += '%02x ' % abs(data[j])
|
||||||
|
|
||||||
line += ' ' * (3 * 16 + 9 - len(line)) + ' | '
|
line += ' ' * (3 * 16 + 9 - len(line)) + ' | '
|
||||||
line += __asc_val(n, data, data_len)
|
|
||||||
|
for j in range(n-16, n):
|
||||||
|
if j >= data_len:
|
||||||
|
break
|
||||||
|
c = data[j] if not (data[j] < 0x20 or data[j] > 0x7e) else '.'
|
||||||
|
line += '%c' % c
|
||||||
|
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
|
|
||||||
tracer.log(level, '\n'.join(lines))
|
tracer.log(level, '\n'.join(lines))
|
||||||
@@ -111,10 +103,11 @@ class Message(metaclass=IterRegistry):
|
|||||||
|
|
||||||
def _update_header(self, _forward_buffer):
|
def _update_header(self, _forward_buffer):
|
||||||
'''callback for updating the header of the forward buffer'''
|
'''callback for updating the header of the forward buffer'''
|
||||||
pass # pragma: no cover
|
return # pragma: no cover
|
||||||
|
|
||||||
def _set_mqtt_timestamp(self, key, ts: float | None):
|
def _set_mqtt_timestamp(self, key, ts: float | None):
|
||||||
if key not in self.new_data or \
|
if type(ts) is not None and \
|
||||||
|
key not in self.new_data or \
|
||||||
not self.new_data[key]:
|
not self.new_data[key]:
|
||||||
if key == 'grid':
|
if key == 'grid':
|
||||||
info_id = Register.TS_GRID
|
info_id = Register.TS_GRID
|
||||||
@@ -135,7 +128,7 @@ class Message(metaclass=IterRegistry):
|
|||||||
if self.mb:
|
if self.mb:
|
||||||
self.mb.close()
|
self.mb.close()
|
||||||
self.mb = None
|
self.mb = None
|
||||||
# pragma: no cover
|
pass # pragma: no cover
|
||||||
|
|
||||||
def inc_counter(self, counter: str) -> None:
|
def inc_counter(self, counter: str) -> None:
|
||||||
self.db.inc_counter(counter)
|
self.db.inc_counter(counter)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class Modbus():
|
|||||||
'''Modbus function code: Write Single Register'''
|
'''Modbus function code: Write Single Register'''
|
||||||
|
|
||||||
__crc_tab = []
|
__crc_tab = []
|
||||||
mb_reg_mapping = {
|
map = {
|
||||||
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||||
0x202c: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
0x202c: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
|
||||||
|
|
||||||
@@ -106,7 +106,6 @@ class Modbus():
|
|||||||
self.loop = asyncio.get_event_loop()
|
self.loop = asyncio.get_event_loop()
|
||||||
self.req_pend = False
|
self.req_pend = False
|
||||||
self.tim = None
|
self.tim = None
|
||||||
self.node_id = ''
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""free the queue and erase the callback handlers"""
|
"""free the queue and erase the callback handlers"""
|
||||||
@@ -139,7 +138,7 @@ class Modbus():
|
|||||||
if self.que.qsize() == 1:
|
if self.que.qsize() == 1:
|
||||||
self.__send_next_from_que()
|
self.__send_next_from_que()
|
||||||
|
|
||||||
def recv_req(self, buf: bytes,
|
def recv_req(self, buf: bytearray,
|
||||||
rsp_handler: Callable[[None], None] = None) -> bool:
|
rsp_handler: Callable[[None], None] = None) -> bool:
|
||||||
"""Add the received Modbus RTU request to the tx queue
|
"""Add the received Modbus RTU request to the tx queue
|
||||||
|
|
||||||
@@ -164,7 +163,7 @@ class Modbus():
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def recv_resp(self, info_db, buf: bytes, node_id: str) -> \
|
def recv_resp(self, info_db, buf: bytearray, node_id: str) -> \
|
||||||
Generator[tuple[str, bool, int | float | str], None, None]:
|
Generator[tuple[str, bool, int | float | str], None, None]:
|
||||||
"""Generator which check and parse a received MODBUS response.
|
"""Generator which check and parse a received MODBUS response.
|
||||||
|
|
||||||
@@ -181,20 +180,58 @@ class Modbus():
|
|||||||
5: No MODBUS request pending
|
5: No MODBUS request pending
|
||||||
"""
|
"""
|
||||||
# logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}')
|
# logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}')
|
||||||
self.node_id = node_id
|
if not self.req_pend:
|
||||||
|
self.err = 5
|
||||||
fcode = buf[1]
|
|
||||||
data_available = self.last_addr == self.INV_ADDR and \
|
|
||||||
(fcode == 3 or fcode == 4)
|
|
||||||
|
|
||||||
if self.__resp_error_check(buf, data_available):
|
|
||||||
return
|
return
|
||||||
|
if not self.__check_crc(buf):
|
||||||
if data_available:
|
logger.error(f'[{node_id}] Modbus resp: CRC error')
|
||||||
|
self.err = 1
|
||||||
|
return
|
||||||
|
if buf[0] != self.last_addr:
|
||||||
|
logger.info(f'[{node_id}] Modbus resp: Wrong addr {buf[0]}')
|
||||||
|
self.err = 2
|
||||||
|
return
|
||||||
|
fcode = buf[1]
|
||||||
|
if fcode != self.last_fcode:
|
||||||
|
logger.info(f'[{node_id}] Modbus: Wrong fcode {fcode}'
|
||||||
|
f' != {self.last_fcode}')
|
||||||
|
self.err = 3
|
||||||
|
return
|
||||||
|
if self.last_addr == self.INV_ADDR and \
|
||||||
|
(fcode == 3 or fcode == 4):
|
||||||
elmlen = buf[2] >> 1
|
elmlen = buf[2] >> 1
|
||||||
|
if elmlen != self.last_len:
|
||||||
|
logger.info(f'[{node_id}] Modbus: len error {elmlen}'
|
||||||
|
f' != {self.last_len}')
|
||||||
|
self.err = 4
|
||||||
|
return
|
||||||
first_reg = self.last_reg # save last_reg before sending next pdu
|
first_reg = self.last_reg # save last_reg before sending next pdu
|
||||||
self.__stop_timer() # stop timer and send next pdu
|
self.__stop_timer() # stop timer and send next pdu
|
||||||
yield from self.__process_data(info_db, buf, first_reg, elmlen)
|
|
||||||
|
for i in range(0, elmlen):
|
||||||
|
addr = first_reg+i
|
||||||
|
if addr in self.map:
|
||||||
|
row = self.map[addr]
|
||||||
|
info_id = row['reg']
|
||||||
|
fmt = row['fmt']
|
||||||
|
val = struct.unpack_from(fmt, buf, 3+2*i)
|
||||||
|
result = val[0]
|
||||||
|
|
||||||
|
if 'eval' in row:
|
||||||
|
result = eval(row['eval'])
|
||||||
|
if 'ratio' in row:
|
||||||
|
result = round(result * row['ratio'], 2)
|
||||||
|
|
||||||
|
keys, level, unit, must_incr = info_db._key_obj(info_id)
|
||||||
|
|
||||||
|
if keys:
|
||||||
|
name, update = info_db.update_db(keys, must_incr,
|
||||||
|
result)
|
||||||
|
yield keys[0], update, result
|
||||||
|
if update:
|
||||||
|
info_db.tracer.log(level,
|
||||||
|
f'[{node_id}] MODBUS: {name}'
|
||||||
|
f' : {result}{unit}')
|
||||||
else:
|
else:
|
||||||
self.__stop_timer()
|
self.__stop_timer()
|
||||||
|
|
||||||
@@ -203,64 +240,6 @@ class Modbus():
|
|||||||
self.rsp_handler()
|
self.rsp_handler()
|
||||||
self.__send_next_from_que()
|
self.__send_next_from_que()
|
||||||
|
|
||||||
def __resp_error_check(self, buf: bytes, data_available: bool) -> bool:
|
|
||||||
'''Check the MODBUS response for errors, returns True if one accure'''
|
|
||||||
if not self.req_pend:
|
|
||||||
self.err = 5
|
|
||||||
return True
|
|
||||||
if not self.__check_crc(buf):
|
|
||||||
logger.error(f'[{self.node_id}] Modbus resp: CRC error')
|
|
||||||
self.err = 1
|
|
||||||
return True
|
|
||||||
if buf[0] != self.last_addr:
|
|
||||||
logger.info(f'[{self.node_id}] Modbus resp: Wrong addr {buf[0]}')
|
|
||||||
self.err = 2
|
|
||||||
return True
|
|
||||||
fcode = buf[1]
|
|
||||||
if fcode != self.last_fcode:
|
|
||||||
logger.info(f'[{self.node_id}] Modbus: Wrong fcode {fcode}'
|
|
||||||
f' != {self.last_fcode}')
|
|
||||||
self.err = 3
|
|
||||||
return True
|
|
||||||
if data_available:
|
|
||||||
elmlen = buf[2] >> 1
|
|
||||||
if elmlen != self.last_len:
|
|
||||||
logger.info(f'[{self.node_id}] Modbus: len error {elmlen}'
|
|
||||||
f' != {self.last_len}')
|
|
||||||
self.err = 4
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __get_value(self, buf: bytes, idx: int, row: dict):
|
|
||||||
'''get a value from the received buffer'''
|
|
||||||
val = struct.unpack_from(row['fmt'], buf, idx)
|
|
||||||
result = val[0]
|
|
||||||
|
|
||||||
if 'eval' in row:
|
|
||||||
result = eval(row['eval'])
|
|
||||||
if 'ratio' in row:
|
|
||||||
result = round(result * row['ratio'], 2)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __process_data(self, info_db, buf: bytes, first_reg, elmlen):
|
|
||||||
'''Generator over received registers, updates the db'''
|
|
||||||
for i in range(0, elmlen):
|
|
||||||
addr = first_reg+i
|
|
||||||
if addr in self.mb_reg_mapping:
|
|
||||||
row = self.mb_reg_mapping[addr]
|
|
||||||
info_id = row['reg']
|
|
||||||
keys, level, unit, must_incr = info_db._key_obj(info_id)
|
|
||||||
if keys:
|
|
||||||
result = self.__get_value(buf, 3+2*i, row)
|
|
||||||
name, update = info_db.update_db(keys, must_incr,
|
|
||||||
result)
|
|
||||||
yield keys[0], update, result
|
|
||||||
if update:
|
|
||||||
info_db.tracer.log(level,
|
|
||||||
f'[{self.node_id}] MODBUS: {name}'
|
|
||||||
f' : {result}{unit}')
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
MODBUS response timer
|
MODBUS response timer
|
||||||
'''
|
'''
|
||||||
@@ -288,10 +267,7 @@ class Modbus():
|
|||||||
self.__start_timer()
|
self.__start_timer()
|
||||||
self.snd_handler(self.last_req, self.last_log_lvl, state='Retrans')
|
self.snd_handler(self.last_req, self.last_log_lvl, state='Retrans')
|
||||||
else:
|
else:
|
||||||
logger.info(f'[{self.node_id}] Modbus timeout '
|
logger.info(f'Modbus timeout {self}')
|
||||||
f'(FCode: {self.last_fcode} '
|
|
||||||
f'Reg: 0x{self.last_reg:04x}, '
|
|
||||||
f'{self.last_len})')
|
|
||||||
self.counter['timeouts'] += 1
|
self.counter['timeouts'] += 1
|
||||||
self.__send_next_from_que()
|
self.__send_next_from_que()
|
||||||
|
|
||||||
@@ -320,11 +296,11 @@ class Modbus():
|
|||||||
'''
|
'''
|
||||||
Helper function for CRC-16 handling
|
Helper function for CRC-16 handling
|
||||||
'''
|
'''
|
||||||
def __check_crc(self, msg: bytes) -> bool:
|
def __check_crc(self, msg: bytearray) -> bool:
|
||||||
'''Check CRC-16 and returns True if valid'''
|
'''Check CRC-16 and returns True if valid'''
|
||||||
return 0 == self.__calc_crc(msg)
|
return 0 == self.__calc_crc(msg)
|
||||||
|
|
||||||
def __calc_crc(self, buffer: bytes) -> int:
|
def __calc_crc(self, buffer: bytearray) -> int:
|
||||||
'''Build CRC-16 for buffer and returns it'''
|
'''Build CRC-16 for buffer and returns it'''
|
||||||
crc = CRC_INIT
|
crc = CRC_INIT
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from config import Config
|
||||||
|
|
||||||
if __name__ == "app.src.modbus_tcp":
|
# import gc
|
||||||
from app.src.config import Config
|
from gen3plus.inverter_g3p import InverterG3P
|
||||||
from app.src.gen3plus.inverter_g3p import InverterG3P
|
|
||||||
else: # pragma: no cover
|
|
||||||
from config import Config
|
|
||||||
from gen3plus.inverter_g3p import InverterG3P
|
|
||||||
|
|
||||||
logger = logging.getLogger('conn')
|
logger = logging.getLogger('conn')
|
||||||
|
|
||||||
@@ -38,9 +35,7 @@ class ModbusConn():
|
|||||||
|
|
||||||
class ModbusTcp():
|
class ModbusTcp():
|
||||||
|
|
||||||
def __init__(self, loop, tim_restart=10) -> None:
|
def __init__(self, loop) -> None:
|
||||||
self.tim_restart = tim_restart
|
|
||||||
|
|
||||||
inverters = Config.get('inverters')
|
inverters = Config.get('inverters')
|
||||||
# logging.info(f'Inverters: {inverters}')
|
# logging.info(f'Inverters: {inverters}')
|
||||||
|
|
||||||
@@ -71,14 +66,11 @@ class ModbusTcp():
|
|||||||
logging.debug(f'Inv-conn:{error}')
|
logging.debug(f'Inv-conn:{error}')
|
||||||
|
|
||||||
except OSError as error:
|
except OSError as error:
|
||||||
if error.errno == 113:
|
logging.info(f'os-error: {error}')
|
||||||
logging.debug(f'os-error:{error}')
|
|
||||||
else:
|
|
||||||
logging.info(f'os-error: {error}')
|
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.error(
|
logging.error(
|
||||||
f"ModbusTcpCreate: Exception for {(host, port)}:\n"
|
f"ModbusTcpCreate: Exception for {(host,port)}:\n"
|
||||||
f"{traceback.format_exc()}")
|
f"{traceback.format_exc()}")
|
||||||
|
|
||||||
await asyncio.sleep(self.tim_restart)
|
await asyncio.sleep(10)
|
||||||
|
|||||||
169
app/src/mqtt.py
169
app/src/mqtt.py
@@ -2,40 +2,26 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import aiomqtt
|
import aiomqtt
|
||||||
import traceback
|
import traceback
|
||||||
if __name__ == "app.src.mqtt":
|
from modbus import Modbus
|
||||||
from app.src.modbus import Modbus
|
from messages import Message
|
||||||
from app.src.messages import Message
|
from config import Config
|
||||||
from app.src.config import Config
|
from singleton import Singleton
|
||||||
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')
|
logger_mqtt = logging.getLogger('mqtt')
|
||||||
|
|
||||||
|
|
||||||
class Mqtt(metaclass=Singleton):
|
class Mqtt(metaclass=Singleton):
|
||||||
__client = None
|
__client = None
|
||||||
__cb_mqtt_is_up = None
|
__cb_MqttIsUp = None
|
||||||
|
|
||||||
def __init__(self, cb_mqtt_is_up):
|
def __init__(self, cb_MqttIsUp):
|
||||||
logger_mqtt.debug('MQTT: __init__')
|
logger_mqtt.debug('MQTT: __init__')
|
||||||
if cb_mqtt_is_up:
|
if cb_MqttIsUp:
|
||||||
self.__cb_mqtt_is_up = cb_mqtt_is_up
|
self.__cb_MqttIsUp = cb_MqttIsUp
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
self.task = loop.create_task(self.__loop())
|
self.task = loop.create_task(self.__loop())
|
||||||
self.ha_restarts = 0
|
self.ha_restarts = 0
|
||||||
|
|
||||||
ha = Config.get('ha')
|
|
||||||
self.ha_status_topic = f"{ha['auto_conf_prefix']}/status"
|
|
||||||
self.mb_rated_topic = f"{ha['entity_prefix']}/+/rated_load"
|
|
||||||
self.mb_out_coeff_topic = f"{ha['entity_prefix']}/+/out_coeff"
|
|
||||||
self.mb_reads_topic = f"{ha['entity_prefix']}/+/modbus_read_regs"
|
|
||||||
self.mb_inputs_topic = f"{ha['entity_prefix']}/+/modbus_read_inputs"
|
|
||||||
self.mb_at_cmd_topic = f"{ha['entity_prefix']}/+/at_cmd"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ha_restarts(self):
|
def ha_restarts(self):
|
||||||
return self._ha_restarts
|
return self._ha_restarts
|
||||||
@@ -63,6 +49,7 @@ class Mqtt(metaclass=Singleton):
|
|||||||
|
|
||||||
async def __loop(self) -> None:
|
async def __loop(self) -> None:
|
||||||
mqtt = Config.get('mqtt')
|
mqtt = Config.get('mqtt')
|
||||||
|
ha = Config.get('ha')
|
||||||
logger_mqtt.info(f'start MQTT: host:{mqtt["host"]} port:'
|
logger_mqtt.info(f'start MQTT: host:{mqtt["host"]} port:'
|
||||||
f'{mqtt["port"]} '
|
f'{mqtt["port"]} '
|
||||||
f'user:{mqtt["user"]}')
|
f'user:{mqtt["user"]}')
|
||||||
@@ -72,24 +59,66 @@ class Mqtt(metaclass=Singleton):
|
|||||||
password=mqtt['passwd'])
|
password=mqtt['passwd'])
|
||||||
|
|
||||||
interval = 5 # Seconds
|
interval = 5 # Seconds
|
||||||
|
ha_status_topic = f"{ha['auto_conf_prefix']}/status"
|
||||||
|
mb_rated_topic = "tsun/+/rated_load" # fixme
|
||||||
|
mb_out_coeff_topic = "tsun/+/out_coeff" # fixme
|
||||||
|
mb_reads_topic = "tsun/+/modbus_read_regs" # fixme
|
||||||
|
mb_inputs_topic = "tsun/+/modbus_read_inputs" # fixme
|
||||||
|
mb_at_cmd_topic = "tsun/+/at_cmd" # fixme
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
async with self.__client:
|
async with self.__client:
|
||||||
logger_mqtt.info('MQTT broker connection established')
|
logger_mqtt.info('MQTT broker connection established')
|
||||||
|
|
||||||
if self.__cb_mqtt_is_up:
|
if self.__cb_MqttIsUp:
|
||||||
await self.__cb_mqtt_is_up()
|
await self.__cb_MqttIsUp()
|
||||||
|
|
||||||
await self.__client.subscribe(self.ha_status_topic)
|
# async with self.__client.messages() as messages:
|
||||||
await self.__client.subscribe(self.mb_rated_topic)
|
await self.__client.subscribe(ha_status_topic)
|
||||||
await self.__client.subscribe(self.mb_out_coeff_topic)
|
await self.__client.subscribe(mb_rated_topic)
|
||||||
await self.__client.subscribe(self.mb_reads_topic)
|
await self.__client.subscribe(mb_out_coeff_topic)
|
||||||
await self.__client.subscribe(self.mb_inputs_topic)
|
await self.__client.subscribe(mb_reads_topic)
|
||||||
await self.__client.subscribe(self.mb_at_cmd_topic)
|
await self.__client.subscribe(mb_inputs_topic)
|
||||||
|
await self.__client.subscribe(mb_at_cmd_topic)
|
||||||
|
|
||||||
async for message in self.__client.messages:
|
async for message in self.__client.messages:
|
||||||
await self.dispatch_msg(message)
|
if message.topic.matches(ha_status_topic):
|
||||||
|
status = message.payload.decode("UTF-8")
|
||||||
|
logger_mqtt.info('Home-Assistant Status:'
|
||||||
|
f' {status}')
|
||||||
|
if status == 'online':
|
||||||
|
self.ha_restarts += 1
|
||||||
|
await self.__cb_MqttIsUp()
|
||||||
|
|
||||||
|
if message.topic.matches(mb_rated_topic):
|
||||||
|
await self.modbus_cmd(message,
|
||||||
|
Modbus.WRITE_SINGLE_REG,
|
||||||
|
1, 0x2008)
|
||||||
|
|
||||||
|
if message.topic.matches(mb_out_coeff_topic):
|
||||||
|
payload = message.payload.decode("UTF-8")
|
||||||
|
val = round(float(payload) * 1024/100)
|
||||||
|
|
||||||
|
if val < 0 or val > 1024:
|
||||||
|
logger_mqtt.error('out_coeff: value must be in'
|
||||||
|
'the range 0..100,'
|
||||||
|
f' got: {payload}')
|
||||||
|
else:
|
||||||
|
await self.modbus_cmd(message,
|
||||||
|
Modbus.WRITE_SINGLE_REG,
|
||||||
|
0, 0x202c, val)
|
||||||
|
|
||||||
|
if message.topic.matches(mb_reads_topic):
|
||||||
|
await self.modbus_cmd(message,
|
||||||
|
Modbus.READ_REGS, 2)
|
||||||
|
|
||||||
|
if message.topic.matches(mb_inputs_topic):
|
||||||
|
await self.modbus_cmd(message,
|
||||||
|
Modbus.READ_INPUTS, 2)
|
||||||
|
|
||||||
|
if message.topic.matches(mb_at_cmd_topic):
|
||||||
|
await self.at_cmd(message)
|
||||||
|
|
||||||
except aiomqtt.MqttError:
|
except aiomqtt.MqttError:
|
||||||
if Config.is_default('mqtt'):
|
if Config.is_default('mqtt'):
|
||||||
@@ -113,76 +142,46 @@ class Mqtt(metaclass=Singleton):
|
|||||||
f"Exception:\n"
|
f"Exception:\n"
|
||||||
f"{traceback.format_exc()}")
|
f"{traceback.format_exc()}")
|
||||||
|
|
||||||
async def dispatch_msg(self, message):
|
|
||||||
if message.topic.matches(self.ha_status_topic):
|
|
||||||
status = message.payload.decode("UTF-8")
|
|
||||||
logger_mqtt.info('Home-Assistant Status:'
|
|
||||||
f' {status}')
|
|
||||||
if status == 'online':
|
|
||||||
self.ha_restarts += 1
|
|
||||||
await self.__cb_mqtt_is_up()
|
|
||||||
|
|
||||||
if message.topic.matches(self.mb_rated_topic):
|
|
||||||
await self.modbus_cmd(message,
|
|
||||||
Modbus.WRITE_SINGLE_REG,
|
|
||||||
1, 0x2008)
|
|
||||||
|
|
||||||
if message.topic.matches(self.mb_out_coeff_topic):
|
|
||||||
payload = message.payload.decode("UTF-8")
|
|
||||||
try:
|
|
||||||
val = round(float(payload) * 1024/100)
|
|
||||||
if val < 0 or val > 1024:
|
|
||||||
logger_mqtt.error('out_coeff: value must be in'
|
|
||||||
'the range 0..100,'
|
|
||||||
f' got: {payload}')
|
|
||||||
else:
|
|
||||||
await self.modbus_cmd(message,
|
|
||||||
Modbus.WRITE_SINGLE_REG,
|
|
||||||
0, 0x202c, val)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if message.topic.matches(self.mb_reads_topic):
|
|
||||||
await self.modbus_cmd(message,
|
|
||||||
Modbus.READ_REGS, 2)
|
|
||||||
|
|
||||||
if message.topic.matches(self.mb_inputs_topic):
|
|
||||||
await self.modbus_cmd(message,
|
|
||||||
Modbus.READ_INPUTS, 2)
|
|
||||||
|
|
||||||
if message.topic.matches(self.mb_at_cmd_topic):
|
|
||||||
await self.at_cmd(message)
|
|
||||||
|
|
||||||
def each_inverter(self, message, func_name: str):
|
def each_inverter(self, message, func_name: str):
|
||||||
topic = str(message.topic)
|
topic = str(message.topic)
|
||||||
node_id = topic.split('/')[1] + '/'
|
node_id = topic.split('/')[1] + '/'
|
||||||
|
found = False
|
||||||
for m in Message:
|
for m in Message:
|
||||||
if m.server_side and (m.node_id == node_id):
|
if m.server_side and (m.node_id == node_id):
|
||||||
|
found = True
|
||||||
logger_mqtt.debug(f'Found: {node_id}')
|
logger_mqtt.debug(f'Found: {node_id}')
|
||||||
fnc = getattr(m, func_name, None)
|
fnc = getattr(m, func_name, None)
|
||||||
if callable(fnc):
|
if callable(fnc):
|
||||||
yield fnc
|
yield fnc
|
||||||
else:
|
else:
|
||||||
logger_mqtt.warning(f'Cmd not supported by: {node_id}')
|
logger_mqtt.warning(f'Cmd not supported by: {node_id}')
|
||||||
break
|
|
||||||
|
|
||||||
else:
|
if not found:
|
||||||
logger_mqtt.warning(f'Node_id: {node_id} not found')
|
logger_mqtt.warning(f'Node_id: {node_id} not found')
|
||||||
|
|
||||||
async def modbus_cmd(self, message, func, params=0, addr=0, val=0):
|
async def modbus_cmd(self, message, func, params=0, addr=0, val=0):
|
||||||
|
topic = str(message.topic)
|
||||||
|
node_id = topic.split('/')[1] + '/'
|
||||||
|
# refactor into a loop over a table
|
||||||
payload = message.payload.decode("UTF-8")
|
payload = message.payload.decode("UTF-8")
|
||||||
for fnc in self.each_inverter(message, "send_modbus_cmd"):
|
logger_mqtt.info(f'MODBUS via MQTT: {topic} = {payload}')
|
||||||
res = payload.split(',')
|
for m in Message:
|
||||||
if params > 0 and params != len(res):
|
if m.server_side and (m.node_id == node_id):
|
||||||
logger_mqtt.error(f'Parameter expected: {params}, '
|
logger_mqtt.debug(f'Found: {node_id}')
|
||||||
f'got: {len(res)}')
|
fnc = getattr(m, "send_modbus_cmd", None)
|
||||||
return
|
res = payload.split(',')
|
||||||
if params == 1:
|
if params > 0 and params != len(res):
|
||||||
val = int(payload)
|
logger_mqtt.error(f'Parameter expected: {params}, '
|
||||||
elif params == 2:
|
f'got: {len(res)}')
|
||||||
addr = int(res[0], base=16)
|
return
|
||||||
val = int(res[1]) # lenght
|
|
||||||
await fnc(func, addr, val, logging.INFO)
|
if callable(fnc):
|
||||||
|
if params == 1:
|
||||||
|
val = int(payload)
|
||||||
|
elif params == 2:
|
||||||
|
addr = int(res[0], base=16)
|
||||||
|
val = int(res[1]) # lenght
|
||||||
|
await fnc(func, addr, val, logging.INFO)
|
||||||
|
|
||||||
async def at_cmd(self, message):
|
async def at_cmd(self, message):
|
||||||
payload = message.payload.decode("UTF-8")
|
payload = message.payload.decode("UTF-8")
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
from weakref import WeakValueDictionary
|
|
||||||
|
|
||||||
|
|
||||||
class Singleton(type):
|
class Singleton(type):
|
||||||
_instances = WeakValueDictionary()
|
_instances = {}
|
||||||
|
|
||||||
def __call__(cls, *args, **kwargs):
|
def __call__(cls, *args, **kwargs):
|
||||||
# logger_mqtt.debug('singleton: __call__')
|
# logger_mqtt.debug('singleton: __call__')
|
||||||
if cls not in cls._instances:
|
if cls not in cls._instances:
|
||||||
instance = super(Singleton,
|
cls._instances[cls] = super(Singleton,
|
||||||
cls).__call__(*args, **kwargs)
|
cls).__call__(*args, **kwargs)
|
||||||
cls._instances[cls] = instance
|
|
||||||
|
|
||||||
return cls._instances[cls]
|
return cls._instances[cls]
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ class TstConfig(Config):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set(cls, cnf):
|
def set(cls, cnf):
|
||||||
cls.act_config = cnf
|
cls.config = cnf
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _read_config_file(cls) -> dict:
|
def _read_config_file(cls) -> dict:
|
||||||
return cls.act_config
|
return cls.config
|
||||||
|
|
||||||
|
|
||||||
def test_empty_config():
|
def test_empty_config():
|
||||||
@@ -20,7 +20,7 @@ def test_empty_config():
|
|||||||
Config.conf_schema.validate(cnf)
|
Config.conf_schema.validate(cnf)
|
||||||
assert False
|
assert False
|
||||||
except SchemaMissingKeyError:
|
except SchemaMissingKeyError:
|
||||||
pass
|
assert True
|
||||||
|
|
||||||
def test_default_config():
|
def test_default_config():
|
||||||
with open("app/config/default_config.toml", "rb") as f:
|
with open("app/config/default_config.toml", "rb") as f:
|
||||||
@@ -28,9 +28,10 @@ def test_default_config():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
validated = Config.conf_schema.validate(cnf)
|
validated = Config.conf_schema.validate(cnf)
|
||||||
except Exception:
|
assert True
|
||||||
|
except:
|
||||||
assert False
|
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': False, 'monitor_sn': 0, 'suggested_area': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': '', 'sensor_list': 688}}}
|
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': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}}
|
||||||
|
|
||||||
def test_full_config():
|
def test_full_config():
|
||||||
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
||||||
@@ -40,13 +41,14 @@ def test_full_config():
|
|||||||
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
|
'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'},
|
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
|
||||||
'inverters': {'allow_all': True,
|
'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'}},
|
'R170000000000001': {'modbus_polling': True, 'node_id': '', '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': ''}}}
|
'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}}
|
||||||
try:
|
try:
|
||||||
validated = Config.conf_schema.validate(cnf)
|
validated = Config.conf_schema.validate(cnf)
|
||||||
except Exception:
|
assert True
|
||||||
|
except:
|
||||||
assert False
|
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, '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}}}
|
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': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}}
|
||||||
|
|
||||||
def test_mininum_config():
|
def test_mininum_config():
|
||||||
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
||||||
@@ -61,9 +63,10 @@ def test_mininum_config():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
validated = Config.conf_schema.validate(cnf)
|
validated = Config.conf_schema.validate(cnf)
|
||||||
except Exception:
|
assert True
|
||||||
|
except:
|
||||||
assert False
|
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}}}
|
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': ''}}}
|
||||||
|
|
||||||
def test_read_empty():
|
def test_read_empty():
|
||||||
cnf = {}
|
cnf = {}
|
||||||
@@ -71,7 +74,7 @@ def test_read_empty():
|
|||||||
err = TstConfig.read('app/config/')
|
err = TstConfig.read('app/config/')
|
||||||
assert err == None
|
assert err == None
|
||||||
cnf = TstConfig.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': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
|
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': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': True, 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
|
||||||
|
|
||||||
defcnf = TstConfig.def_config.get('solarman')
|
defcnf = TstConfig.def_config.get('solarman')
|
||||||
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||||
@@ -93,7 +96,7 @@ def test_read_cnf1():
|
|||||||
err = TstConfig.read('app/config/')
|
err = TstConfig.read('app/config/')
|
||||||
assert err == None
|
assert err == None
|
||||||
cnf = TstConfig.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': 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': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
|
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': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': True, 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
|
||||||
cnf = TstConfig.get('solarman')
|
cnf = TstConfig.get('solarman')
|
||||||
assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||||
defcnf = TstConfig.def_config.get('solarman')
|
defcnf = TstConfig.def_config.get('solarman')
|
||||||
@@ -106,7 +109,7 @@ def test_read_cnf2():
|
|||||||
err = TstConfig.read('app/config/')
|
err = TstConfig.read('app/config/')
|
||||||
assert err == None
|
assert err == None
|
||||||
cnf = TstConfig.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': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
|
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': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': True, 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
|
||||||
assert True == TstConfig.is_default('solarman')
|
assert True == TstConfig.is_default('solarman')
|
||||||
|
|
||||||
def test_read_cnf3():
|
def test_read_cnf3():
|
||||||
@@ -123,7 +126,7 @@ def test_read_cnf4():
|
|||||||
err = TstConfig.read('app/config/')
|
err = TstConfig.read('app/config/')
|
||||||
assert err == None
|
assert err == None
|
||||||
cnf = TstConfig.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': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
|
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': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': True, 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
|
||||||
assert False == TstConfig.is_default('solarman')
|
assert False == TstConfig.is_default('solarman')
|
||||||
|
|
||||||
def test_read_cnf5():
|
def test_read_cnf5():
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# test_with_pytest.py
|
# test_with_pytest.py
|
||||||
import pytest
|
import pytest
|
||||||
import json, math
|
import json
|
||||||
import logging
|
import logging
|
||||||
from app.src.infos import Register, ClrAtMidnight
|
from app.src.infos import Register, ClrAtMidnight
|
||||||
from app.src.infos import Infos
|
from app.src.infos import Infos
|
||||||
@@ -77,7 +77,7 @@ def test_table_definition():
|
|||||||
|
|
||||||
|
|
||||||
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
||||||
pass # sideeffect is calling generator i.ha_proxy_confs()
|
pass
|
||||||
|
|
||||||
val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter
|
val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter
|
||||||
assert val == 0
|
assert val == 0
|
||||||
@@ -222,24 +222,24 @@ def test_get_value():
|
|||||||
|
|
||||||
i.set_db_def_value(Register.PV2_VOLTAGE, 30.3)
|
i.set_db_def_value(Register.PV2_VOLTAGE, 30.3)
|
||||||
assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None)
|
assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None)
|
||||||
assert math.isclose(30.3,i.get_db_value(Register.PV2_VOLTAGE, None), rel_tol=1e-09, abs_tol=1e-09)
|
assert 30.3 == i.get_db_value(Register.PV2_VOLTAGE, None)
|
||||||
|
|
||||||
def test_update_value():
|
def test_update_value():
|
||||||
i = Infos()
|
i = Infos()
|
||||||
assert None == i.get_db_value(Register.PV1_VOLTAGE, None)
|
assert None == i.get_db_value(Register.PV1_VOLTAGE, None)
|
||||||
|
|
||||||
keys = i.info_defs[Register.PV1_VOLTAGE]['name']
|
keys = i.info_defs[Register.PV1_VOLTAGE]['name']
|
||||||
_, update = i.update_db(keys, True, 30)
|
name, update = i.update_db(keys, True, 30)
|
||||||
assert update == True
|
assert update == True
|
||||||
assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None)
|
assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None)
|
||||||
|
|
||||||
keys = i.info_defs[Register.PV1_VOLTAGE]['name']
|
keys = i.info_defs[Register.PV1_VOLTAGE]['name']
|
||||||
_, update = i.update_db(keys, True, 30)
|
name, update = i.update_db(keys, True, 30)
|
||||||
assert update == False
|
assert update == False
|
||||||
assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None)
|
assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None)
|
||||||
|
|
||||||
keys = i.info_defs[Register.PV1_VOLTAGE]['name']
|
keys = i.info_defs[Register.PV1_VOLTAGE]['name']
|
||||||
_, update = i.update_db(keys, False, 29)
|
name, update = i.update_db(keys, False, 29)
|
||||||
assert update == True
|
assert update == True
|
||||||
assert 29 == i.get_db_value(Register.PV1_VOLTAGE, None)
|
assert 29 == i.get_db_value(Register.PV1_VOLTAGE, None)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from app.src.infos import Register, ClrAtMidnight
|
|||||||
from app.src.gen3.infos_g3 import InfosG3
|
from app.src.gen3.infos_g3 import InfosG3
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def contr_data_seq(): # Get Time Request message
|
def ContrDataSeq(): # Get Time Request message
|
||||||
msg = b'\x00\x00\x00\x15\x00\x09\x2b\xa8\x54\x10\x52\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x30\x36\x00\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f'
|
msg = b'\x00\x00\x00\x15\x00\x09\x2b\xa8\x54\x10\x52\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x30\x36\x00\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f'
|
||||||
msg += b'\x6e\x00\x09\x2f\x90\x54\x0b\x52\x53\x57\x2d\x31\x2d\x31\x30\x30\x30\x31\x00\x09\x5a\x88\x54\x0f\x74\x2e\x72\x61\x79\x6d\x6f\x6e\x69\x6f\x74\x2e\x63\x6f\x6d\x00\x09\x5a\xec\x54'
|
msg += b'\x6e\x00\x09\x2f\x90\x54\x0b\x52\x53\x57\x2d\x31\x2d\x31\x30\x30\x30\x31\x00\x09\x5a\x88\x54\x0f\x74\x2e\x72\x61\x79\x6d\x6f\x6e\x69\x6f\x74\x2e\x63\x6f\x6d\x00\x09\x5a\xec\x54'
|
||||||
msg += b'\x1c\x6c\x6f\x67\x67\x65\x72\x2e\x74\x61\x6c\x65\x6e\x74\x2d\x6d\x6f\x6e\x69\x74\x6f\x72\x69\x6e\x67\x2e\x63\x6f\x6d\x00\x0d\x00\x20\x49\x00\x00\x00\x01\x00\x0c\x35\x00\x49\x00'
|
msg += b'\x1c\x6c\x6f\x67\x67\x65\x72\x2e\x74\x61\x6c\x65\x6e\x74\x2d\x6d\x6f\x6e\x69\x74\x6f\x72\x69\x6e\x67\x2e\x63\x6f\x6d\x00\x0d\x00\x20\x49\x00\x00\x00\x01\x00\x0c\x35\x00\x49\x00'
|
||||||
@@ -14,7 +14,7 @@ def contr_data_seq(): # Get Time Request message
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def contr2_data_seq(): # Get Time Request message
|
def Contr2DataSeq(): # Get Time Request message
|
||||||
msg = b'\x00\x00\x00\x39\x00\x09\x2b\xa8\x54\x10\x52'
|
msg = b'\x00\x00\x00\x39\x00\x09\x2b\xa8\x54\x10\x52'
|
||||||
msg += b'\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x32\x30\x00'
|
msg += b'\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x32\x30\x00'
|
||||||
msg += b'\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f\x6e\x00\x09\x2f\x90\x54'
|
msg += b'\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f\x6e\x00\x09\x2f\x90\x54'
|
||||||
@@ -94,19 +94,19 @@ def contr2_data_seq(): # Get Time Request message
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def inv_data_seq(): # Data indication from the controller
|
def InvDataSeq(): # Data indication from the controller
|
||||||
msg = b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
|
msg = b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
|
||||||
msg += b'\x54\x10T170000000000001\x00\x00\x00\x32\x54\x0a\x54\x53\x4f\x4c\x2d\x4d\x53\x36\x30\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43'
|
msg += b'\x54\x10T170000000000001\x00\x00\x00\x32\x54\x0a\x54\x53\x4f\x4c\x2d\x4d\x53\x36\x30\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def invalid_data_seq(): # Data indication from the controller
|
def InvalidDataSeq(): # Data indication from the controller
|
||||||
msg = b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x64\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
|
msg = b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x64\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
|
||||||
msg += b'\x54\x10T170000000000001\x00\x00\x00\x32\x54\x0a\x54\x53\x4f\x4c\x2d\x4d\x53\x36\x30\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43'
|
msg += b'\x54\x10T170000000000001\x00\x00\x00\x32\x54\x0a\x54\x53\x4f\x4c\x2d\x4d\x53\x36\x30\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43'
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def inv_data_seq2(): # Data indication from the controller
|
def InvDataSeq2(): # Data indication from the controller
|
||||||
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00'
|
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00'
|
||||||
msg += b'\x00\x00\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00'
|
msg += b'\x00\x00\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00'
|
||||||
msg += b'\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53'
|
msg += b'\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53'
|
||||||
@@ -141,7 +141,7 @@ def inv_data_seq2(): # Data indication from the controller
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def inv_data_new(): # Data indication from DSP V5.0.17
|
def InvDataNew(): # Data indication from DSP V5.0.17
|
||||||
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x00\x53\x00\x00'
|
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x00\x53\x00\x00'
|
||||||
msg += b'\x00\x00\x00\x80\x53\x00\x00\x00\x00\x01\x04\x53\x00\x00\x00\x00'
|
msg += b'\x00\x00\x00\x80\x53\x00\x00\x00\x00\x01\x04\x53\x00\x00\x00\x00'
|
||||||
msg += b'\x01\x90\x41\x00\x00\x01\x91\x53\x00\x00\x00\x00\x01\x90\x53\x00'
|
msg += b'\x01\x90\x41\x00\x00\x01\x91\x53\x00\x00\x00\x00\x01\x90\x53\x00'
|
||||||
@@ -217,7 +217,7 @@ def inv_data_new(): # Data indication from DSP V5.0.17
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def inv_data_seq2_zero(): # Data indication from the controller
|
def InvDataSeq2_Zero(): # Data indication from the controller
|
||||||
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00'
|
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00'
|
||||||
msg += b'\x00\x00\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00'
|
msg += b'\x00\x00\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00'
|
||||||
msg += b'\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53'
|
msg += b'\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53'
|
||||||
@@ -252,37 +252,37 @@ def inv_data_seq2_zero(): # Data indication from the controller
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
||||||
def test_parse_control(contr_data_seq):
|
def test_parse_control(ContrDataSeq):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
for key, result in i.parse (contr_data_seq):
|
for key, result in i.parse (ContrDataSeq):
|
||||||
pass # side effect in calling i.parse()
|
pass
|
||||||
|
|
||||||
assert json.dumps(i.db) == json.dumps(
|
assert json.dumps(i.db) == json.dumps(
|
||||||
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 100, "Power_On_Time": 29, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
|
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 100, "Power_On_Time": 29, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
|
||||||
|
|
||||||
def test_parse_control2(contr2_data_seq):
|
def test_parse_control2(Contr2DataSeq):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
for key, result in i.parse (contr2_data_seq):
|
for key, result in i.parse (Contr2DataSeq):
|
||||||
pass # side effect in calling i.parse()
|
pass
|
||||||
|
|
||||||
assert json.dumps(i.db) == json.dumps(
|
assert json.dumps(i.db) == json.dumps(
|
||||||
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.20", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 16, "Power_On_Time": 334, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
|
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.20", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 16, "Power_On_Time": 334, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
|
||||||
|
|
||||||
def test_parse_inverter(inv_data_seq):
|
def test_parse_inverter(InvDataSeq):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
for key, result in i.parse (inv_data_seq):
|
for key, result in i.parse (InvDataSeq):
|
||||||
pass # side effect in calling i.parse()
|
pass
|
||||||
|
|
||||||
assert json.dumps(i.db) == json.dumps(
|
assert json.dumps(i.db) == json.dumps(
|
||||||
{"inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}})
|
{"inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}})
|
||||||
|
|
||||||
def test_parse_cont_and_invert(contr_data_seq, inv_data_seq):
|
def test_parse_cont_and_invert(ContrDataSeq, InvDataSeq):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
for key, result in i.parse (contr_data_seq):
|
for key, result in i.parse (ContrDataSeq):
|
||||||
pass # side effect in calling i.parse()
|
pass
|
||||||
|
|
||||||
for key, result in i.parse (inv_data_seq):
|
for key, result in i.parse (InvDataSeq):
|
||||||
pass # side effect in calling i.parse()
|
pass
|
||||||
|
|
||||||
assert json.dumps(i.db) == json.dumps(
|
assert json.dumps(i.db) == json.dumps(
|
||||||
{
|
{
|
||||||
@@ -290,7 +290,7 @@ def test_parse_cont_and_invert(contr_data_seq, inv_data_seq):
|
|||||||
"inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}})
|
"inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}})
|
||||||
|
|
||||||
|
|
||||||
def test_build_ha_conf1(contr_data_seq):
|
def test_build_ha_conf1(ContrDataSeq):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
i.static_init() # initialize counter
|
i.static_init() # initialize counter
|
||||||
|
|
||||||
@@ -325,11 +325,7 @@ def test_build_ha_conf1(contr_data_seq):
|
|||||||
|
|
||||||
assert tests==4
|
assert tests==4
|
||||||
|
|
||||||
def test_build_ha_conf2(contr_data_seq):
|
|
||||||
i = InfosG3()
|
|
||||||
i.static_init() # initialize counter
|
|
||||||
|
|
||||||
tests = 0
|
|
||||||
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
||||||
|
|
||||||
if id == 'out_power_123':
|
if id == 'out_power_123':
|
||||||
@@ -348,16 +344,16 @@ def test_build_ha_conf2(contr_data_seq):
|
|||||||
assert d_json == json.dumps({"name": "Active Inverter Connections", "stat_t": "tsun/proxy/proxy", "dev_cla": None, "stat_cla": None, "uniq_id": "inv_count_456", "val_tpl": "{{value_json['Inverter_Cnt'] | int}}", "ic": "mdi:counter", "dev": {"name": "Proxy", "sa": "Proxy", "mdl": "proxy", "mf": "Stefan Allius", "sw": "unknown", "ids": ["proxy"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
assert d_json == json.dumps({"name": "Active Inverter Connections", "stat_t": "tsun/proxy/proxy", "dev_cla": None, "stat_cla": None, "uniq_id": "inv_count_456", "val_tpl": "{{value_json['Inverter_Cnt'] | int}}", "ic": "mdi:counter", "dev": {"name": "Proxy", "sa": "Proxy", "mdl": "proxy", "mf": "Stefan Allius", "sw": "unknown", "ids": ["proxy"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
assert tests==1
|
assert tests==5
|
||||||
|
|
||||||
def test_build_ha_conf3(contr_data_seq, inv_data_seq, inv_data_seq2):
|
def test_build_ha_conf2(ContrDataSeq, InvDataSeq, InvDataSeq2):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
for key, result in i.parse (contr_data_seq):
|
for key, result in i.parse (ContrDataSeq):
|
||||||
pass # side effect in calling i.parse()
|
pass
|
||||||
for key, result in i.parse (inv_data_seq):
|
for key, result in i.parse (InvDataSeq):
|
||||||
pass # side effect in calling i.parse()
|
pass
|
||||||
for key, result in i.parse (inv_data_seq2):
|
for key, result in i.parse (InvDataSeq2):
|
||||||
pass # side effect in calling i.parse()
|
pass
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'):
|
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'):
|
||||||
@@ -388,10 +384,10 @@ def test_build_ha_conf3(contr_data_seq, inv_data_seq, inv_data_seq2):
|
|||||||
tests +=1
|
tests +=1
|
||||||
assert tests==5
|
assert tests==5
|
||||||
|
|
||||||
def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
|
def test_must_incr_total(InvDataSeq2, InvDataSeq2_Zero):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (inv_data_seq2):
|
for key, update in i.parse (InvDataSeq2):
|
||||||
if key == 'total' or key == 'inverter' or key == 'env':
|
if key == 'total' or key == 'inverter' or key == 'env':
|
||||||
assert update == True
|
assert update == True
|
||||||
tests +=1
|
tests +=1
|
||||||
@@ -400,8 +396,11 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
|
|||||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23})
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23})
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (inv_data_seq2):
|
for key, update in i.parse (InvDataSeq2):
|
||||||
if key == 'total' or key == 'env':
|
if key == 'total':
|
||||||
|
assert update == False
|
||||||
|
tests +=1
|
||||||
|
elif key == 'env':
|
||||||
assert update == False
|
assert update == False
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
@@ -412,7 +411,7 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
|
|||||||
assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "No_Inputs": 2})
|
assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "No_Inputs": 2})
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (inv_data_seq2_zero):
|
for key, update in i.parse (InvDataSeq2_Zero):
|
||||||
if key == 'total':
|
if key == 'total':
|
||||||
assert update == False
|
assert update == False
|
||||||
tests +=1
|
tests +=1
|
||||||
@@ -425,10 +424,10 @@ def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
|
|||||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
|
||||||
|
|
||||||
def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
|
def test_must_incr_total2(InvDataSeq2, InvDataSeq2_Zero):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (inv_data_seq2_zero):
|
for key, update in i.parse (InvDataSeq2_Zero):
|
||||||
if key == 'total':
|
if key == 'total':
|
||||||
assert update == False
|
assert update == False
|
||||||
tests +=1
|
tests +=1
|
||||||
@@ -442,8 +441,11 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
|
|||||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (inv_data_seq2_zero):
|
for key, update in i.parse (InvDataSeq2_Zero):
|
||||||
if key == 'total' or key == 'env':
|
if key == 'total':
|
||||||
|
assert update == False
|
||||||
|
tests +=1
|
||||||
|
elif key == 'env':
|
||||||
assert update == False
|
assert update == False
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
@@ -453,8 +455,11 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
|
|||||||
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (inv_data_seq2):
|
for key, update in i.parse (InvDataSeq2):
|
||||||
if key == 'total' or key == 'env':
|
if key == 'total':
|
||||||
|
assert update == True
|
||||||
|
tests +=1
|
||||||
|
elif key == 'env':
|
||||||
assert update == True
|
assert update == True
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
@@ -462,10 +467,10 @@ def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
|
|||||||
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||||
|
|
||||||
def test_new_data_types(inv_data_new):
|
def test_new_data_types(InvDataNew):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (inv_data_new):
|
for key, update in i.parse (InvDataNew):
|
||||||
if key == 'events':
|
if key == 'events':
|
||||||
tests +=1
|
tests +=1
|
||||||
elif key == 'inverter':
|
elif key == 'inverter':
|
||||||
@@ -482,7 +487,7 @@ def test_new_data_types(inv_data_new):
|
|||||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {}})
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {}})
|
||||||
assert json.dumps(i.db['events']) == json.dumps({"401_": 0, "404_": 0, "405_": 0, "408_": 0, "409_No_Utility": 0, "406_": 0, "416_": 0})
|
assert json.dumps(i.db['events']) == json.dumps({"401_": 0, "404_": 0, "405_": 0, "408_": 0, "409_No_Utility": 0, "406_": 0, "416_": 0})
|
||||||
|
|
||||||
def test_invalid_data_type(invalid_data_seq):
|
def test_invalid_data_type(InvalidDataSeq):
|
||||||
i = InfosG3()
|
i = InfosG3()
|
||||||
i.static_init() # initialize counter
|
i.static_init() # initialize counter
|
||||||
|
|
||||||
@@ -490,8 +495,8 @@ def test_invalid_data_type(invalid_data_seq):
|
|||||||
assert val == 0
|
assert val == 0
|
||||||
|
|
||||||
|
|
||||||
for key, result in i.parse (invalid_data_seq):
|
for key, result in i.parse (InvalidDataSeq):
|
||||||
pass # side effect in calling i.parse()
|
pass
|
||||||
assert json.dumps(i.db) == json.dumps({"inverter": {"Product_Name": "Microinv"}})
|
assert json.dumps(i.db) == json.dumps({"inverter": {"Product_Name": "Microinv"}})
|
||||||
|
|
||||||
val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter
|
val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter
|
||||||
|
|||||||
@@ -1,33 +1,18 @@
|
|||||||
|
|
||||||
# test_with_pytest.py
|
# test_with_pytest.py
|
||||||
import pytest, json, math, random
|
import pytest, json
|
||||||
from app.src.infos import Register
|
from app.src.infos import Register
|
||||||
from app.src.gen3plus.infos_g3p import InfosG3P
|
from app.src.gen3plus.infos_g3p import InfosG3P
|
||||||
from app.src.gen3plus.infos_g3p import RegisterMap
|
from app.src.gen3plus.infos_g3p import RegisterMap
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def str_test_ip():
|
|
||||||
ip = ".".join(str(random.randint(1, 254)) for _ in range(4))
|
|
||||||
print(f'random_ip: {ip}')
|
|
||||||
return ip
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def bytes_test_ip(str_test_ip):
|
|
||||||
ip = bytes(str.encode(str_test_ip))
|
|
||||||
l = len(ip)
|
|
||||||
if l < 16:
|
|
||||||
ip = ip + bytearray(16-l)
|
|
||||||
print(f'random_ip: {ip}')
|
|
||||||
return ip
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def device_data(bytes_test_ip): # 0x4110 ftype: 0x02
|
def DeviceData(): # 0x4110 ftype: 0x02
|
||||||
msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xba\xd2\x00\x00'
|
msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xba\xd2\x00\x00'
|
||||||
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
|
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
|
||||||
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
|
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
|
||||||
msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
msg += b'\x30\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54' + bytes_test_ip
|
msg += b'\x00\x00\x00\x00\x00\x00\x40\x2a\x8f\x4f\x51\x54\x31\x39\x32\x2e'
|
||||||
msg += b'\x0f\x00\x01\xb0'
|
msg += b'\x31\x36\x38\x2e\x38\x30\x2e\x34\x39\x00\x00\x00\x0f\x00\x01\xb0'
|
||||||
msg += b'\x02\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00'
|
msg += b'\x02\x0f\x00\xff\x56\x31\x2e\x31\x2e\x30\x30\x2e\x30\x42\x00\x00'
|
||||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00'
|
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\x00\x00'
|
||||||
@@ -39,7 +24,7 @@ def device_data(bytes_test_ip): # 0x4110 ftype: 0x02
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def inverter_data(): # 0x4210 ftype: 0x01
|
def InverterData(): # 0x4210 ftype: 0x01
|
||||||
msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xb0\x02\xbc\xc8'
|
msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xb0\x02\xbc\xc8'
|
||||||
msg += b'\x24\x32\x6c\x1f\x00\x00\xa0\x47\xe4\x33\x01\x00\x03\x08\x00\x00'
|
msg += b'\x24\x32\x6c\x1f\x00\x00\xa0\x47\xe4\x33\x01\x00\x03\x08\x00\x00'
|
||||||
msg += b'\x59\x31\x37\x45\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x45'
|
msg += b'\x59\x31\x37\x45\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x45'
|
||||||
@@ -78,26 +63,26 @@ def test_default_db():
|
|||||||
"collector": {"Chip_Type": "IGEN TECH"},
|
"collector": {"Chip_Type": "IGEN TECH"},
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_parse_4110(str_test_ip, device_data: bytes):
|
def test_parse_4110(DeviceData: bytes):
|
||||||
i = InfosG3P(client_mode=False)
|
i = InfosG3P(client_mode=False)
|
||||||
i.db.clear()
|
i.db.clear()
|
||||||
for key, update in i.parse (device_data, 0x41, 2):
|
for key, update in i.parse (DeviceData, 0x41, 2):
|
||||||
pass # side effect is calling generator i.parse()
|
pass
|
||||||
|
|
||||||
assert json.dumps(i.db) == json.dumps({
|
assert json.dumps(i.db) == json.dumps({
|
||||||
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": str_test_ip, "Sensor_List": "02b0"},
|
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": "192.168.80.49"},
|
||||||
'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "Collector_Fw_Version": "V1.1.00.0B"},
|
'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "Collector_Fw_Version": "V1.1.00.0B"},
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_parse_4210(inverter_data: bytes):
|
def test_parse_4210(InverterData: bytes):
|
||||||
i = InfosG3P(client_mode=False)
|
i = InfosG3P(client_mode=False)
|
||||||
i.db.clear()
|
i.db.clear()
|
||||||
|
|
||||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
for key, update in i.parse (InverterData, 0x42, 1):
|
||||||
pass # side effect is calling generator i.parse()
|
pass
|
||||||
|
|
||||||
assert json.dumps(i.db) == json.dumps({
|
assert json.dumps(i.db) == json.dumps({
|
||||||
"controller": {"Sensor_List": "02b0", "Power_On_Time": 2051},
|
"controller": {"Power_On_Time": 2051},
|
||||||
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000},
|
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000},
|
||||||
"env": {"Inverter_Status": 1, "Inverter_Temp": 14},
|
"env": {"Inverter_Status": 1, "Inverter_Temp": 14},
|
||||||
"grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8},
|
"grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8},
|
||||||
@@ -154,11 +139,7 @@ def test_build_ha_conf1():
|
|||||||
|
|
||||||
assert tests==7
|
assert tests==7
|
||||||
|
|
||||||
def test_build_ha_conf2():
|
|
||||||
i = InfosG3P(client_mode=False)
|
|
||||||
i.static_init() # initialize counter
|
|
||||||
|
|
||||||
tests = 0
|
|
||||||
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
||||||
|
|
||||||
if id == 'out_power_123':
|
if id == 'out_power_123':
|
||||||
@@ -180,9 +161,9 @@ def test_build_ha_conf2():
|
|||||||
assert d_json == json.dumps({"name": "Active Inverter Connections", "stat_t": "tsun/proxy/proxy", "dev_cla": None, "stat_cla": None, "uniq_id": "inv_count_456", "val_tpl": "{{value_json['Inverter_Cnt'] | int}}", "ic": "mdi:counter", "dev": {"name": "Proxy", "sa": "Proxy", "mdl": "proxy", "mf": "Stefan Allius", "sw": "unknown", "ids": ["proxy"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
assert d_json == json.dumps({"name": "Active Inverter Connections", "stat_t": "tsun/proxy/proxy", "dev_cla": None, "stat_cla": None, "uniq_id": "inv_count_456", "val_tpl": "{{value_json['Inverter_Cnt'] | int}}", "ic": "mdi:counter", "dev": {"name": "Proxy", "sa": "Proxy", "mdl": "proxy", "mf": "Stefan Allius", "sw": "unknown", "ids": ["proxy"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
assert tests==1
|
assert tests==8
|
||||||
|
|
||||||
def test_build_ha_conf3():
|
def test_build_ha_conf2():
|
||||||
i = InfosG3P(client_mode=True)
|
i = InfosG3P(client_mode=True)
|
||||||
i.static_init() # initialize counter
|
i.static_init() # initialize counter
|
||||||
|
|
||||||
@@ -228,11 +209,7 @@ def test_build_ha_conf3():
|
|||||||
|
|
||||||
assert tests==7
|
assert tests==7
|
||||||
|
|
||||||
def test_build_ha_conf4():
|
|
||||||
i = InfosG3P(client_mode=True)
|
|
||||||
i.static_init() # initialize counter
|
|
||||||
|
|
||||||
tests = 0
|
|
||||||
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
|
||||||
|
|
||||||
if id == 'out_power_123':
|
if id == 'out_power_123':
|
||||||
@@ -254,29 +231,30 @@ def test_build_ha_conf4():
|
|||||||
assert d_json == json.dumps({"name": "Active Inverter Connections", "stat_t": "tsun/proxy/proxy", "dev_cla": None, "stat_cla": None, "uniq_id": "inv_count_456", "val_tpl": "{{value_json['Inverter_Cnt'] | int}}", "ic": "mdi:counter", "dev": {"name": "Proxy", "sa": "Proxy", "mdl": "proxy", "mf": "Stefan Allius", "sw": "unknown", "ids": ["proxy"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
assert d_json == json.dumps({"name": "Active Inverter Connections", "stat_t": "tsun/proxy/proxy", "dev_cla": None, "stat_cla": None, "uniq_id": "inv_count_456", "val_tpl": "{{value_json['Inverter_Cnt'] | int}}", "ic": "mdi:counter", "dev": {"name": "Proxy", "sa": "Proxy", "mdl": "proxy", "mf": "Stefan Allius", "sw": "unknown", "ids": ["proxy"]}, "o": {"name": "proxy", "sw": "unknown"}})
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
assert tests==1
|
assert tests==8
|
||||||
|
|
||||||
def test_exception_and_eval(inverter_data: bytes):
|
def test_exception_and_eval(InverterData: bytes):
|
||||||
|
|
||||||
# add eval to convert temperature from °F to °C
|
# add eval to convert temperature from °F to °C
|
||||||
RegisterMap.map[0x420100d8]['eval'] = '(result-32)/1.8'
|
RegisterMap.map[0x420100d8]['eval'] = '(result-32)/1.8'
|
||||||
# map PV1_VOLTAGE to invalid register
|
# map PV1_VOLTAGE to invalid register
|
||||||
RegisterMap.map[0x420100e0]['reg'] = Register.TEST_REG2
|
RegisterMap.map[0x420100e0]['reg'] = Register.TEST_REG2
|
||||||
# set invalid maping entry for OUTPUT_POWER (string instead of dict type)
|
# set invalid maping entry for OUTPUT_POWER (string instead of dict type)
|
||||||
backup = RegisterMap.map[0x420100de]
|
Backup = RegisterMap.map[0x420100de]
|
||||||
RegisterMap.map[0x420100de] = 'invalid_entry'
|
RegisterMap.map[0x420100de] = 'invalid_entry'
|
||||||
|
|
||||||
i = InfosG3P(client_mode=False)
|
i = InfosG3P(client_mode=False)
|
||||||
# i.db.clear()
|
# i.db.clear()
|
||||||
|
|
||||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
for key, update in i.parse (InverterData, 0x42, 1):
|
||||||
pass # side effect is calling generator i.parse()
|
pass
|
||||||
assert math.isclose(12.2222, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09)
|
assert 12.2222 == round (i.get_db_value(Register.INVERTER_TEMP, 0),4)
|
||||||
|
|
||||||
del RegisterMap.map[0x420100d8]['eval'] # remove eval
|
del RegisterMap.map[0x420100d8]['eval'] # remove eval
|
||||||
RegisterMap.map[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping
|
RegisterMap.map[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping
|
||||||
RegisterMap.map[0x420100de] = backup # reset mapping
|
RegisterMap.map[0x420100de] = Backup # reset mapping
|
||||||
|
|
||||||
for key, update in i.parse (inverter_data, 0x42, 1):
|
for key, update in i.parse (InverterData, 0x42, 1):
|
||||||
pass # side effect is calling generator i.parse()
|
pass
|
||||||
assert 54 == i.get_db_value(Register.INVERTER_TEMP, 0)
|
assert 54 == i.get_db_value(Register.INVERTER_TEMP, 0)
|
||||||
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
# test_with_pytest.py
|
|
||||||
import pytest
|
|
||||||
import asyncio
|
|
||||||
import aiomqtt
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from mock import patch, Mock
|
|
||||||
from app.src.singleton import Singleton
|
|
||||||
from app.src.inverter import Inverter
|
|
||||||
from app.src.mqtt import Mqtt
|
|
||||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
|
||||||
from app.src.config import Config
|
|
||||||
|
|
||||||
|
|
||||||
pytest_plugins = ('pytest_asyncio',)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module", autouse=True)
|
|
||||||
def module_init():
|
|
||||||
def new_init(cls, cb_mqtt_is_up):
|
|
||||||
cb_mqtt_is_up()
|
|
||||||
|
|
||||||
Singleton._instances.clear()
|
|
||||||
with patch.object(Mqtt, '__init__', new_init):
|
|
||||||
yield
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def test_port():
|
|
||||||
return 1883
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def test_hostname():
|
|
||||||
# if getenv("GITHUB_ACTIONS") == "true":
|
|
||||||
# return 'mqtt'
|
|
||||||
# else:
|
|
||||||
return 'test.mosquitto.org'
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def config_conn(test_hostname, test_port):
|
|
||||||
Config.act_config = {
|
|
||||||
'mqtt':{
|
|
||||||
'host': test_hostname,
|
|
||||||
'port': test_port,
|
|
||||||
'user': '',
|
|
||||||
'passwd': ''
|
|
||||||
},
|
|
||||||
'ha':{
|
|
||||||
'auto_conf_prefix': 'homeassistant',
|
|
||||||
'discovery_prefix': 'homeassistant',
|
|
||||||
'entity_prefix': 'tsun',
|
|
||||||
'proxy_node_id': 'test_1',
|
|
||||||
'proxy_unique_id': ''
|
|
||||||
},
|
|
||||||
'inverters': {
|
|
||||||
'allow_all': True,
|
|
||||||
"R170000000000001":{
|
|
||||||
'node_id': 'inv_1'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_inverter_cb(config_conn):
|
|
||||||
_ = config_conn
|
|
||||||
|
|
||||||
with patch.object(Inverter, '_cb_mqtt_is_up', wraps=Inverter._cb_mqtt_is_up) as spy:
|
|
||||||
print('call Inverter.class_init')
|
|
||||||
Inverter.class_init()
|
|
||||||
assert 'homeassistant/' == Inverter.discovery_prfx
|
|
||||||
assert 'tsun/' == Inverter.entity_prfx
|
|
||||||
assert 'test_1/' == Inverter.proxy_node_id
|
|
||||||
spy.assert_called_once()
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_mqtt_is_up(config_conn):
|
|
||||||
_ = config_conn
|
|
||||||
|
|
||||||
with patch.object(Mqtt, 'publish') as spy:
|
|
||||||
Inverter.class_init()
|
|
||||||
await Inverter._cb_mqtt_is_up()
|
|
||||||
spy.assert_called()
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_mqtt_proxy_statt_invalid(config_conn):
|
|
||||||
_ = config_conn
|
|
||||||
|
|
||||||
with patch.object(Mqtt, 'publish') as spy:
|
|
||||||
Inverter.class_init()
|
|
||||||
await Inverter._async_publ_mqtt_proxy_stat('InValId_kEy')
|
|
||||||
spy.assert_not_called()
|
|
||||||
@@ -5,6 +5,7 @@ from app.src.modbus import Modbus
|
|||||||
from app.src.infos import Infos, Register
|
from app.src.infos import Infos, Register
|
||||||
|
|
||||||
pytest_plugins = ('pytest_asyncio',)
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
|
# pytestmark = pytest.mark.asyncio(scope="module")
|
||||||
|
|
||||||
class ModbusTestHelper(Modbus):
|
class ModbusTestHelper(Modbus):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -75,8 +76,8 @@ def test_recv_resp_crc_err():
|
|||||||
mb.req_pend = True
|
mb.req_pend = True
|
||||||
mb.last_addr = 1
|
mb.last_addr = 1
|
||||||
mb.last_fcode = 3
|
mb.last_fcode = 3
|
||||||
mb.last_reg = 0x300e
|
mb.last_reg == 0x300e
|
||||||
mb.last_len = 2
|
mb.last_len == 2
|
||||||
# check matching response, but with CRC error
|
# check matching response, but with CRC error
|
||||||
call = 0
|
call = 0
|
||||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf3', 'test'):
|
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf3', 'test'):
|
||||||
@@ -95,8 +96,8 @@ def test_recv_resp_invalid_addr():
|
|||||||
# simulate a transmitted request
|
# simulate a transmitted request
|
||||||
mb.last_addr = 1
|
mb.last_addr = 1
|
||||||
mb.last_fcode = 3
|
mb.last_fcode = 3
|
||||||
mb.last_reg = 0x300e
|
mb.last_reg == 0x300e
|
||||||
mb.last_len = 2
|
mb.last_len == 2
|
||||||
|
|
||||||
# check not matching response, with wrong server addr
|
# check not matching response, with wrong server addr
|
||||||
call = 0
|
call = 0
|
||||||
@@ -246,7 +247,7 @@ def test_queue2():
|
|||||||
assert mb.pdu == b'\x01\x06\x20\x08\x00\x04\x02\x0b'
|
assert mb.pdu == b'\x01\x06\x20\x08\x00\x04\x02\x0b'
|
||||||
|
|
||||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x06\x20\x08\x00\x04\x02\x0b', 'test'):
|
for key, update, val in mb.recv_resp(mb.db, b'\x01\x06\x20\x08\x00\x04\x02\x0b', 'test'):
|
||||||
pass # call generator mb.recv_resp()
|
pass
|
||||||
|
|
||||||
assert mb.que.qsize() == 0
|
assert mb.que.qsize() == 0
|
||||||
assert mb.send_calls == 3
|
assert mb.send_calls == 3
|
||||||
@@ -297,7 +298,7 @@ def test_queue3():
|
|||||||
assert mb.pdu == b'\x01\x06\x20\x08\x00\x04\x02\x0b'
|
assert mb.pdu == b'\x01\x06\x20\x08\x00\x04\x02\x0b'
|
||||||
|
|
||||||
for key, update, val in mb.recv_resp(mb.db, b'\x01\x06\x20\x08\x00\x04\x02\x0b', 'test'):
|
for key, update, val in mb.recv_resp(mb.db, b'\x01\x06\x20\x08\x00\x04\x02\x0b', 'test'):
|
||||||
pass # no code in loop is OK; calling the generator is the purpose
|
pass
|
||||||
assert 0 == mb.err
|
assert 0 == mb.err
|
||||||
assert mb.recv_responses == 2
|
assert mb.recv_responses == 2
|
||||||
|
|
||||||
@@ -363,11 +364,13 @@ async def test_timeout():
|
|||||||
assert mb.retry_cnt == 0
|
assert mb.retry_cnt == 0
|
||||||
assert mb.send_calls == 4
|
assert mb.send_calls == 4
|
||||||
|
|
||||||
|
# assert mb.counter == {}
|
||||||
|
|
||||||
def test_recv_unknown_data():
|
def test_recv_unknown_data():
|
||||||
'''Receive a response with an unknwon register'''
|
'''Receive a response with an unknwon register'''
|
||||||
mb = ModbusTestHelper()
|
mb = ModbusTestHelper()
|
||||||
assert 0x9000 not in mb.mb_reg_mapping
|
assert 0x9000 not in mb.map
|
||||||
mb.mb_reg_mapping[0x9000] = {'reg': Register.TEST_REG1, 'fmt': '!H', 'ratio': 1}
|
mb.map[0x9000] = {'reg': Register.TEST_REG1, 'fmt': '!H', 'ratio': 1}
|
||||||
|
|
||||||
mb.build_msg(1,3,0x9000,2)
|
mb.build_msg(1,3,0x9000,2)
|
||||||
|
|
||||||
@@ -379,7 +382,7 @@ def test_recv_unknown_data():
|
|||||||
assert 0 == call
|
assert 0 == call
|
||||||
assert not mb.req_pend
|
assert not mb.req_pend
|
||||||
|
|
||||||
del mb.mb_reg_mapping[0x9000]
|
del mb.map[0x9000]
|
||||||
|
|
||||||
def test_close():
|
def test_close():
|
||||||
'''Check queue handling for build_msg() calls'''
|
'''Check queue handling for build_msg() calls'''
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
# test_with_pytest.py
|
|
||||||
import pytest
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from mock import patch
|
|
||||||
from enum import Enum
|
|
||||||
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.messages import Message, State
|
|
||||||
from app.src.inverter import Inverter
|
|
||||||
from app.src.modbus_tcp import ModbusConn, ModbusTcp
|
|
||||||
|
|
||||||
|
|
||||||
pytest_plugins = ('pytest_asyncio',)
|
|
||||||
|
|
||||||
# initialize the proxy statistics
|
|
||||||
Infos.static_init()
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module", autouse=True)
|
|
||||||
def module_init():
|
|
||||||
Singleton._instances.clear()
|
|
||||||
yield
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def test_port():
|
|
||||||
return 1883
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def test_hostname():
|
|
||||||
# if getenv("GITHUB_ACTIONS") == "true":
|
|
||||||
# return 'mqtt'
|
|
||||||
# else:
|
|
||||||
return 'test.mosquitto.org'
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def config_conn(test_hostname, test_port):
|
|
||||||
Config.act_config = {
|
|
||||||
'mqtt':{
|
|
||||||
'host': test_hostname,
|
|
||||||
'port': test_port,
|
|
||||||
'user': '',
|
|
||||||
'passwd': ''
|
|
||||||
},
|
|
||||||
'ha':{
|
|
||||||
'auto_conf_prefix': 'homeassistant',
|
|
||||||
'discovery_prefix': 'homeassistant',
|
|
||||||
'entity_prefix': 'tsun',
|
|
||||||
'proxy_node_id': 'test_1',
|
|
||||||
'proxy_unique_id': ''
|
|
||||||
},
|
|
||||||
'inverters':{
|
|
||||||
'allow_all': True,
|
|
||||||
"R170000000000001":{
|
|
||||||
'node_id': 'inv_1'
|
|
||||||
},
|
|
||||||
"Y170000000000001":{
|
|
||||||
'node_id': 'inv_2',
|
|
||||||
'monitor_sn': 2000000000,
|
|
||||||
'modbus_polling': True,
|
|
||||||
'suggested_area': "",
|
|
||||||
'sensor_list': 0x2b0,
|
|
||||||
'client_mode':{
|
|
||||||
'host': '192.168.0.1',
|
|
||||||
'port': 8899
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestType(Enum):
|
|
||||||
RD_TEST_0_BYTES = 1
|
|
||||||
RD_TEST_TIMEOUT = 2
|
|
||||||
|
|
||||||
|
|
||||||
test = TestType.RD_TEST_0_BYTES
|
|
||||||
|
|
||||||
class FakeReader():
|
|
||||||
def __init__(self):
|
|
||||||
self.on_recv = asyncio.Event()
|
|
||||||
async def read(self, max_len: int):
|
|
||||||
await self.on_recv.wait()
|
|
||||||
if test == TestType.RD_TEST_0_BYTES:
|
|
||||||
return b''
|
|
||||||
elif test == TestType.RD_TEST_TIMEOUT:
|
|
||||||
raise TimeoutError
|
|
||||||
def feed_eof(self):
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
class FakeWriter():
|
|
||||||
def write(self, buf: bytes):
|
|
||||||
return
|
|
||||||
def get_extra_info(self, sel: str):
|
|
||||||
if sel == 'peername':
|
|
||||||
return 'remote.intern'
|
|
||||||
elif sel == 'sockname':
|
|
||||||
return 'sock:1234'
|
|
||||||
assert False
|
|
||||||
def is_closing(self):
|
|
||||||
return False
|
|
||||||
def close(self):
|
|
||||||
return
|
|
||||||
async def wait_closed(self):
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def patch_open():
|
|
||||||
async def new_conn(conn):
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
return FakeReader(), FakeWriter()
|
|
||||||
|
|
||||||
def new_open(host: str, port: int):
|
|
||||||
global test
|
|
||||||
if test == TestType.RD_TEST_TIMEOUT:
|
|
||||||
raise TimeoutError
|
|
||||||
return new_conn(None)
|
|
||||||
|
|
||||||
with patch.object(asyncio, 'open_connection', new_open) as conn:
|
|
||||||
yield conn
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def patch_no_mqtt():
|
|
||||||
with patch.object(Mqtt, 'publish') as conn:
|
|
||||||
yield conn
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_modbus_conn(patch_open):
|
|
||||||
_ = patch_open
|
|
||||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
|
||||||
|
|
||||||
async with ModbusConn('test.local', 1234) as stream:
|
|
||||||
assert stream.node_id == 'G3P'
|
|
||||||
assert stream.addr == ('test.local', 1234)
|
|
||||||
assert type(stream.reader) is FakeReader
|
|
||||||
assert type(stream.writer) is FakeWriter
|
|
||||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 1
|
|
||||||
|
|
||||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_modbus_no_cnf():
|
|
||||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
ModbusTcp(loop)
|
|
||||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_modbus_cnf1(config_conn, patch_open):
|
|
||||||
_ = config_conn
|
|
||||||
_ = patch_open
|
|
||||||
global test
|
|
||||||
assert asyncio.get_running_loop()
|
|
||||||
Inverter.class_init()
|
|
||||||
test = TestType.RD_TEST_TIMEOUT
|
|
||||||
|
|
||||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
ModbusTcp(loop)
|
|
||||||
await asyncio.sleep(0.01)
|
|
||||||
for m in Message:
|
|
||||||
if (m.node_id == 'inv_2'):
|
|
||||||
assert False
|
|
||||||
|
|
||||||
await asyncio.sleep(0.01)
|
|
||||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_modbus_cnf2(config_conn, patch_no_mqtt, patch_open):
|
|
||||||
_ = config_conn
|
|
||||||
_ = patch_open
|
|
||||||
_ = patch_no_mqtt
|
|
||||||
global test
|
|
||||||
assert asyncio.get_running_loop()
|
|
||||||
Inverter.class_init()
|
|
||||||
test = TestType.RD_TEST_0_BYTES
|
|
||||||
|
|
||||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
|
||||||
ModbusTcp(asyncio.get_event_loop())
|
|
||||||
await asyncio.sleep(0.01)
|
|
||||||
test = 0
|
|
||||||
for m in Message:
|
|
||||||
if (m.node_id == 'inv_2'):
|
|
||||||
test += 1
|
|
||||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 1
|
|
||||||
m.shutdown_started = True
|
|
||||||
m.reader.on_recv.set()
|
|
||||||
del m
|
|
||||||
|
|
||||||
assert 1 == test
|
|
||||||
await asyncio.sleep(0.01)
|
|
||||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
|
||||||
# check that the connection is released
|
|
||||||
for m in Message:
|
|
||||||
if (m.node_id == 'inv_2'):
|
|
||||||
assert False
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_modbus_cnf3(config_conn, patch_no_mqtt, patch_open):
|
|
||||||
_ = config_conn
|
|
||||||
_ = patch_open
|
|
||||||
_ = patch_no_mqtt
|
|
||||||
global test
|
|
||||||
assert asyncio.get_running_loop()
|
|
||||||
Inverter.class_init()
|
|
||||||
test = TestType.RD_TEST_0_BYTES
|
|
||||||
|
|
||||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
|
||||||
ModbusTcp(asyncio.get_event_loop(), tim_restart= 0)
|
|
||||||
await asyncio.sleep(0.01)
|
|
||||||
test = 0
|
|
||||||
for m in Message:
|
|
||||||
if (m.node_id == 'inv_2'):
|
|
||||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 1
|
|
||||||
test += 1
|
|
||||||
if test == 1:
|
|
||||||
m.shutdown_started = False
|
|
||||||
m.reader.on_recv.set()
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
assert m.state == State.closed
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
else:
|
|
||||||
m.shutdown_started = True
|
|
||||||
m.reader.on_recv.set()
|
|
||||||
del m
|
|
||||||
|
|
||||||
assert 2 == test
|
|
||||||
await asyncio.sleep(0.01)
|
|
||||||
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
# test_with_pytest.py
|
|
||||||
import pytest
|
|
||||||
import asyncio
|
|
||||||
import aiomqtt
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from mock import patch, Mock
|
|
||||||
from app.src.singleton import Singleton
|
|
||||||
from app.src.mqtt import Mqtt
|
|
||||||
from app.src.modbus import Modbus
|
|
||||||
from app.src.gen3plus.solarman_v5 import SolarmanV5
|
|
||||||
from app.src.config import Config
|
|
||||||
|
|
||||||
|
|
||||||
pytest_plugins = ('pytest_asyncio',)
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module", autouse=True)
|
|
||||||
def module_init():
|
|
||||||
Singleton._instances.clear()
|
|
||||||
yield
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def test_port():
|
|
||||||
return 1883
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def test_hostname():
|
|
||||||
# if getenv("GITHUB_ACTIONS") == "true":
|
|
||||||
# return 'mqtt'
|
|
||||||
# else:
|
|
||||||
return 'test.mosquitto.org'
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def config_mqtt_conn(test_hostname, test_port):
|
|
||||||
Config.act_config = {'mqtt':{'host': test_hostname, 'port': test_port, 'user': '', 'passwd': ''},
|
|
||||||
'ha':{'auto_conf_prefix': 'homeassistant','discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun'}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def config_no_conn(test_port):
|
|
||||||
Config.act_config = {'mqtt':{'host': "", 'port': test_port, 'user': '', 'passwd': ''},
|
|
||||||
'ha':{'auto_conf_prefix': 'homeassistant','discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun'}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def spy_at_cmd():
|
|
||||||
conn = SolarmanV5(server_side=True, client_mode= False)
|
|
||||||
conn.node_id = 'inv_2/'
|
|
||||||
with patch.object(conn, 'send_at_cmd', wraps=conn.send_at_cmd) as wrapped_conn:
|
|
||||||
yield wrapped_conn
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def spy_modbus_cmd():
|
|
||||||
conn = SolarmanV5(server_side=True, client_mode= False)
|
|
||||||
conn.node_id = 'inv_1/'
|
|
||||||
with patch.object(conn, 'send_modbus_cmd', wraps=conn.send_modbus_cmd) as wrapped_conn:
|
|
||||||
yield wrapped_conn
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def spy_modbus_cmd_client():
|
|
||||||
conn = SolarmanV5(server_side=False, client_mode= False)
|
|
||||||
conn.node_id = 'inv_1/'
|
|
||||||
with patch.object(conn, 'send_modbus_cmd', wraps=conn.send_modbus_cmd) as wrapped_conn:
|
|
||||||
yield wrapped_conn
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
def test_native_client(test_hostname, test_port):
|
|
||||||
"""Sanity check: Make sure the paho-mqtt client can connect to the test
|
|
||||||
MQTT server.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import paho.mqtt.client as mqtt
|
|
||||||
import threading
|
|
||||||
|
|
||||||
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)
|
|
||||||
assert on_connect.wait(5)
|
|
||||||
finally:
|
|
||||||
c.loop_stop()
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_mqtt_no_config(config_no_conn):
|
|
||||||
_ = config_no_conn
|
|
||||||
assert asyncio.get_running_loop()
|
|
||||||
|
|
||||||
on_connect = asyncio.Event()
|
|
||||||
async def cb():
|
|
||||||
on_connect.set()
|
|
||||||
|
|
||||||
try:
|
|
||||||
m = Mqtt(cb)
|
|
||||||
assert m.task
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
assert not on_connect.is_set()
|
|
||||||
try:
|
|
||||||
await m.publish('homeassistant/status', 'online')
|
|
||||||
assert False
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
except TimeoutError:
|
|
||||||
assert False
|
|
||||||
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
|
|
||||||
spy = spy_modbus_cmd
|
|
||||||
try:
|
|
||||||
m = Mqtt(None)
|
|
||||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/rated_load', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
|
|
||||||
await m.dispatch_msg(msg)
|
|
||||||
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x2008, 2, logging.INFO)
|
|
||||||
|
|
||||||
spy.reset_mock()
|
|
||||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'100', qos= 0, retain = False, mid= 0, properties= None)
|
|
||||||
await m.dispatch_msg(msg)
|
|
||||||
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x202c, 1024, logging.INFO)
|
|
||||||
|
|
||||||
spy.reset_mock()
|
|
||||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'50', qos= 0, retain = False, mid= 0, properties= None)
|
|
||||||
await m.dispatch_msg(msg)
|
|
||||||
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x202c, 512, logging.INFO)
|
|
||||||
|
|
||||||
spy.reset_mock()
|
|
||||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_regs', payload= b'0x3000, 10', qos= 0, retain = False, mid= 0, properties= None)
|
|
||||||
await m.dispatch_msg(msg)
|
|
||||||
spy.assert_awaited_once_with(Modbus.READ_REGS, 0x3000, 10, logging.INFO)
|
|
||||||
|
|
||||||
spy.reset_mock()
|
|
||||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_inputs', payload= b'0x3000, 10', qos= 0, retain = False, mid= 0, properties= None)
|
|
||||||
await m.dispatch_msg(msg)
|
|
||||||
spy.assert_awaited_once_with(Modbus.READ_INPUTS, 0x3000, 10, logging.INFO)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
await m.close()
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_msg_dispatch_err(config_mqtt_conn, spy_modbus_cmd):
|
|
||||||
_ = config_mqtt_conn
|
|
||||||
spy = spy_modbus_cmd
|
|
||||||
try:
|
|
||||||
m = Mqtt(None)
|
|
||||||
# test out of range param
|
|
||||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'-1', qos= 0, retain = False, mid= 0, properties= None)
|
|
||||||
await m.dispatch_msg(msg)
|
|
||||||
spy.assert_not_called()
|
|
||||||
|
|
||||||
# test unknown node_id
|
|
||||||
spy.reset_mock()
|
|
||||||
msg = aiomqtt.Message(topic= 'tsun/inv_2/out_coeff', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
|
|
||||||
await m.dispatch_msg(msg)
|
|
||||||
spy.assert_not_called()
|
|
||||||
|
|
||||||
# test invalid fload param
|
|
||||||
spy.reset_mock()
|
|
||||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'2, 3', qos= 0, retain = False, mid= 0, properties= None)
|
|
||||||
await m.dispatch_msg(msg)
|
|
||||||
spy.assert_not_called()
|
|
||||||
|
|
||||||
spy.reset_mock()
|
|
||||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_regs', payload= b'0x3000, 10, 7', qos= 0, retain = False, mid= 0, properties= None)
|
|
||||||
await m.dispatch_msg(msg)
|
|
||||||
spy.assert_not_called()
|
|
||||||
finally:
|
|
||||||
await m.close()
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_msg_ignore_client_conn(config_mqtt_conn, spy_modbus_cmd_client):
|
|
||||||
'''don't call function if connnection is not in server mode'''
|
|
||||||
_ = config_mqtt_conn
|
|
||||||
spy = spy_modbus_cmd_client
|
|
||||||
try:
|
|
||||||
m = Mqtt(None)
|
|
||||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/rated_load', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
|
|
||||||
await m.dispatch_msg(msg)
|
|
||||||
spy.assert_not_called()
|
|
||||||
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'''
|
|
||||||
_ = config_mqtt_conn
|
|
||||||
try:
|
|
||||||
m = Mqtt(None)
|
|
||||||
msg = aiomqtt.Message(topic= 'tsun/inv_1/rated_load', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
|
|
||||||
for _ in m.each_inverter(msg, 'unkown_fnc'):
|
|
||||||
assert False
|
|
||||||
finally:
|
|
||||||
await m.close()
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_at_cmd_dispatch(config_mqtt_conn, spy_at_cmd):
|
|
||||||
_ = config_mqtt_conn
|
|
||||||
spy = spy_at_cmd
|
|
||||||
try:
|
|
||||||
m = Mqtt(None)
|
|
||||||
msg = aiomqtt.Message(topic= 'tsun/inv_2/at_cmd', payload= b'AT+', qos= 0, retain = False, mid= 0, properties= None)
|
|
||||||
await m.dispatch_msg(msg)
|
|
||||||
spy.assert_awaited_once_with('AT+')
|
|
||||||
|
|
||||||
finally:
|
|
||||||
await m.close()
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# test_with_pytest.py
|
|
||||||
import pytest
|
|
||||||
from app.src.singleton import Singleton
|
|
||||||
|
|
||||||
class Test(metaclass=Singleton):
|
|
||||||
def __init__(self):
|
|
||||||
pass # is a dummy test class
|
|
||||||
|
|
||||||
def test_singleton_metaclass():
|
|
||||||
Singleton._instances.clear()
|
|
||||||
a = Test()
|
|
||||||
assert 1 == len(Singleton._instances)
|
|
||||||
b = Test()
|
|
||||||
assert 1 == len(Singleton._instances)
|
|
||||||
assert a is b
|
|
||||||
del a
|
|
||||||
assert 1 == len(Singleton._instances)
|
|
||||||
del b
|
|
||||||
assert 0 == len(Singleton._instances)
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
-r ./app/requirements-test.txt
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
sonar.projectKey=s-allius_tsun-gen3-proxy
|
|
||||||
sonar.organization=s-allius
|
|
||||||
|
|
||||||
# This is the name and version displayed in the SonarCloud UI.
|
|
||||||
sonar.projectName=tsun-gen3-proxy
|
|
||||||
#sonar.projectVersion=1.0
|
|
||||||
|
|
||||||
|
|
||||||
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
|
|
||||||
sonar.sources=app/src/
|
|
||||||
|
|
||||||
# Encoding of the source code. Default is default system encoding
|
|
||||||
#sonar.sourceEncoding=UTF-8
|
|
||||||
|
|
||||||
sonar.python.version=3.12
|
|
||||||
sonar.tests=system_tests/,app/tests/
|
|
||||||
sonar.exclusions=**/.vscode/**/*
|
|
||||||
# Name your criteria
|
|
||||||
sonar.issue.ignore.multicriteria=e1,e2
|
|
||||||
|
|
||||||
# python:S905 : Remove or refactor this statement; it has no side effects
|
|
||||||
sonar.issue.ignore.multicriteria.e1.ruleKey=python:S905
|
|
||||||
sonar.issue.ignore.multicriteria.e1.resourceKey=app/tests/*.py
|
|
||||||
|
|
||||||
sonar.issue.ignore.multicriteria.e2.ruleKey=python:S905
|
|
||||||
sonar.issue.ignore.multicriteria.e2.resourceKey=systems_tests/*.py
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# test_with_pytest.py and scapy
|
# test_with_pytest.py and scapy
|
||||||
#
|
#
|
||||||
import pytest, socket, time
|
import pytest, socket, time
|
||||||
|
#from scapy.all import *
|
||||||
|
#from scapy.layers.inet import IP, TCP, TCP_client
|
||||||
|
|
||||||
def get_sn() -> bytes:
|
def get_sn() -> bytes:
|
||||||
return b'R170000000000001'
|
return b'R170000000000001'
|
||||||
@@ -13,31 +15,31 @@ def get_invalid_sn():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def msg_contact_info(): # Contact Info message
|
def MsgContactInfo(): # Contact Info message
|
||||||
return b'\x00\x00\x00\x2c\x10'+get_sn()+b'\x91\x00\x08solarhub\x0fsolarhub\x40123456'
|
return b'\x00\x00\x00\x2c\x10'+get_sn()+b'\x91\x00\x08solarhub\x0fsolarhub\x40123456'
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def msg_contact_resp(): # Contact Response message
|
def MsgContactResp(): # Contact Response message
|
||||||
return b'\x00\x00\x00\x14\x10'+get_sn()+b'\x91\x00\x01'
|
return b'\x00\x00\x00\x14\x10'+get_sn()+b'\x91\x00\x01'
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def msg_contact_info2(): # Contact Info message
|
def MsgContactInfo2(): # Contact Info message
|
||||||
return b'\x00\x00\x00\x2c\x10'+get_invalid_sn()+b'\x91\x00\x08solarhub\x0fsolarhub\x40123456'
|
return b'\x00\x00\x00\x2c\x10'+get_invalid_sn()+b'\x91\x00\x08solarhub\x0fsolarhub\x40123456'
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def msg_contact_resp2(): # Contact Response message
|
def MsgContactResp2(): # Contact Response message
|
||||||
return b'\x00\x00\x00\x14\x10'+get_invalid_sn()+b'\x91\x00\x01'
|
return b'\x00\x00\x00\x14\x10'+get_invalid_sn()+b'\x91\x00\x01'
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def msg_timestamp_req(): # Get Time Request message
|
def MsgTimeStampReq(): # Get Time Request message
|
||||||
return b'\x00\x00\x00\x13\x10'+get_sn()+b'\x91\x22'
|
return b'\x00\x00\x00\x13\x10'+get_sn()+b'\x91\x22'
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def msg_timestamp_resp(): # Get Time Resonse message
|
def MsgTimeStampResp(): # Get Time Resonse message
|
||||||
return b'\x00\x00\x00\x1b\x10'+get_sn()+b'\x99\x22\x00\x00\x01\x89\xc6\x63\x4d\x80'
|
return b'\x00\x00\x00\x1b\x10'+get_sn()+b'\x99\x22\x00\x00\x01\x89\xc6\x63\x4d\x80'
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def msg_controller_ind(): # Data indication from the controller
|
def MsgContollerInd(): # Data indication from the controller
|
||||||
msg = b'\x00\x00\x01\x2f\x10'+ get_sn() + b'\x91\x71\x0e\x10\x00\x00\x10'+get_sn()
|
msg = b'\x00\x00\x01\x2f\x10'+ get_sn() + b'\x91\x71\x0e\x10\x00\x00\x10'+get_sn()
|
||||||
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x55\x50'
|
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x55\x50'
|
||||||
msg += b'\x00\x00\x00\x15\x00\x09\x2b\xa8\x54\x10\x52\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x30\x36\x00\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f'
|
msg += b'\x00\x00\x00\x15\x00\x09\x2b\xa8\x54\x10\x52\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x30\x36\x00\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f'
|
||||||
@@ -49,7 +51,7 @@ def msg_controller_ind(): # Data indication from the controller
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def msg_inv_data(): # Data indication from the controller
|
def MsgInvData(): # Data indication from the controller
|
||||||
msg = b'\x00\x00\x00\x8b\x10'+ get_sn() + b'\x91\x04\x01\x90\x00\x01\x10'+get_inv_no()
|
msg = b'\x00\x00\x00\x8b\x10'+ get_sn() + b'\x91\x04\x01\x90\x00\x01\x10'+get_inv_no()
|
||||||
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x61\x08'
|
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x61\x08'
|
||||||
msg += b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
|
msg += b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
|
||||||
@@ -57,7 +59,7 @@ def msg_inv_data(): # Data indication from the controller
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def msg_inverter_ind(): # Data indication from the inverter
|
def MsgInverterInd(): # Data indication from the inverter
|
||||||
msg = b'\x00\x00\x05\x02\x10'+ get_sn() + b'\x91\x04\x01\x90\x00\x01\x10'+get_inv_no()
|
msg = b'\x00\x00\x05\x02\x10'+ get_sn() + b'\x91\x04\x01\x90\x00\x01\x10'+get_inv_no()
|
||||||
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x61\x08'
|
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x61\x08'
|
||||||
msg += b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00'
|
msg += b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00'
|
||||||
@@ -94,7 +96,7 @@ def msg_inverter_ind(): # Data indication from the inverter
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def msg_ota_update_req(): # Over the air update request from talent cloud
|
def MsgOtaUpdateReq(): # Over the air update request from talent cloud
|
||||||
msg = b'\x00\x00\x01\x16\x10'+ get_sn() + b'\x70\x13\x01\x02\x76\x35'
|
msg = b'\x00\x00\x01\x16\x10'+ get_sn() + b'\x70\x13\x01\x02\x76\x35'
|
||||||
msg += b'\x70\x68\x74\x74\x70'
|
msg += b'\x70\x68\x74\x74\x70'
|
||||||
msg += b'\x3a\x2f\x2f\x77\x77\x77\x2e\x74\x61\x6c\x65\x6e\x74\x2d\x6d\x6f'
|
msg += b'\x3a\x2f\x2f\x77\x77\x77\x2e\x74\x61\x6c\x65\x6e\x74\x2d\x6d\x6f'
|
||||||
@@ -117,8 +119,10 @@ def msg_ota_update_req(): # Over the air update request from talent cloud
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def client_connection():
|
def ClientConnection():
|
||||||
|
#host = '172.16.30.7'
|
||||||
host = 'logger.talent-monitoring.com'
|
host = 'logger.talent-monitoring.com'
|
||||||
|
#host = '127.0.0.1'
|
||||||
port = 5005
|
port = 5005
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
s.connect((host, port))
|
s.connect((host, port))
|
||||||
@@ -127,8 +131,10 @@ def client_connection():
|
|||||||
time.sleep(2.5)
|
time.sleep(2.5)
|
||||||
s.close()
|
s.close()
|
||||||
|
|
||||||
def tempclient_connection():
|
def tempClientConnection():
|
||||||
|
#host = '172.16.30.7'
|
||||||
host = 'logger.talent-monitoring.com'
|
host = 'logger.talent-monitoring.com'
|
||||||
|
#host = '127.0.0.1'
|
||||||
port = 5005
|
port = 5005
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
s.connect((host, port))
|
s.connect((host, port))
|
||||||
@@ -138,99 +144,100 @@ def tempclient_connection():
|
|||||||
|
|
||||||
def test_open_close():
|
def test_open_close():
|
||||||
try:
|
try:
|
||||||
for _ in tempclient_connection():
|
for s in tempClientConnection():
|
||||||
pass # test side effect of generator
|
pass
|
||||||
except Exception:
|
except:
|
||||||
assert False
|
assert False
|
||||||
|
assert True
|
||||||
|
|
||||||
def test_send_contact_info1(client_connection, msg_contact_info, msg_contact_resp):
|
def test_send_contact_info1(ClientConnection, MsgContactInfo, MsgContactResp):
|
||||||
s = client_connection
|
s = ClientConnection
|
||||||
try:
|
try:
|
||||||
s.sendall(msg_contact_info)
|
s.sendall(MsgContactInfo)
|
||||||
data = s.recv(1024)
|
data = s.recv(1024)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
assert data == msg_contact_resp
|
assert data == MsgContactResp
|
||||||
|
|
||||||
|
|
||||||
def test_send_contact_info2(client_connection, msg_contact_info2, msg_contact_info, msg_contact_resp):
|
def test_send_contact_info2(ClientConnection, MsgContactInfo2, MsgContactInfo, MsgContactResp):
|
||||||
s = client_connection
|
s = ClientConnection
|
||||||
try:
|
try:
|
||||||
s.sendall(msg_contact_info2)
|
s.sendall(MsgContactInfo2)
|
||||||
data = s.recv(1024)
|
data = s.recv(1024)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
assert True
|
||||||
else:
|
else:
|
||||||
assert False
|
assert False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
s.sendall(msg_contact_info)
|
s.sendall(MsgContactInfo)
|
||||||
data = s.recv(1024)
|
data = s.recv(1024)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
assert data == msg_contact_resp
|
assert data == MsgContactResp
|
||||||
|
|
||||||
def test_send_contact_info3(client_connection, msg_contact_info, msg_contact_resp, msg_timestamp_req):
|
def test_send_contact_info3(ClientConnection, MsgContactInfo, MsgContactResp, MsgTimeStampReq):
|
||||||
s = client_connection
|
s = ClientConnection
|
||||||
try:
|
try:
|
||||||
s.sendall(msg_contact_info)
|
s.sendall(MsgContactInfo)
|
||||||
data = s.recv(1024)
|
data = s.recv(1024)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
assert data == msg_contact_resp
|
assert data == MsgContactResp
|
||||||
try:
|
try:
|
||||||
s.sendall(msg_timestamp_req)
|
s.sendall(MsgTimeStampReq)
|
||||||
data = s.recv(1024)
|
data = s.recv(1024)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_send_contact_resp(client_connection, msg_contact_resp):
|
def test_send_contact_resp(ClientConnection, MsgContactResp):
|
||||||
s = client_connection
|
s = ClientConnection
|
||||||
try:
|
try:
|
||||||
s.sendall(msg_contact_resp)
|
s.sendall(MsgContactResp)
|
||||||
data = s.recv(1024)
|
data = s.recv(1024)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
assert True
|
||||||
else:
|
else:
|
||||||
assert data == b''
|
assert data == b''
|
||||||
|
|
||||||
def test_send_ctrl_data(client_connection, msg_timestamp_req, msg_timestamp_resp, msg_controller_ind):
|
def test_send_ctrl_data(ClientConnection, MsgTimeStampReq, MsgTimeStampResp, MsgContollerInd):
|
||||||
s = client_connection
|
s = ClientConnection
|
||||||
try:
|
try:
|
||||||
s.sendall(msg_timestamp_req)
|
s.sendall(MsgTimeStampReq)
|
||||||
_ = s.recv(1024)
|
data = s.recv(1024)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
# time.sleep(2.5)
|
# time.sleep(2.5)
|
||||||
# assert data == msg_timestamp_resp
|
# assert data == MsgTimeStampResp
|
||||||
try:
|
try:
|
||||||
s.sendall(msg_controller_ind)
|
s.sendall(MsgContollerInd)
|
||||||
_ = s.recv(1024)
|
data = s.recv(1024)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_send_inv_data(client_connection, msg_timestamp_req, msg_timestamp_resp, msg_inv_data, msg_inverter_ind):
|
def test_send_inv_data(ClientConnection, MsgTimeStampReq, MsgTimeStampResp, MsgInvData, MsgInverterInd):
|
||||||
s = client_connection
|
s = ClientConnection
|
||||||
try:
|
try:
|
||||||
s.sendall(msg_timestamp_req)
|
s.sendall(MsgTimeStampReq)
|
||||||
_ = s.recv(1024)
|
data = s.recv(1024)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
# time.sleep(32.5)
|
# time.sleep(32.5)
|
||||||
# assert data == msg_timestamp_resp
|
# assert data == MsgTimeStampResp
|
||||||
try:
|
try:
|
||||||
s.sendall(msg_inv_data)
|
s.sendall(MsgInvData)
|
||||||
_ = s.recv(1024)
|
data = s.recv(1024)
|
||||||
s.sendall(msg_inverter_ind)
|
s.sendall(MsgInverterInd)
|
||||||
_ = s.recv(1024)
|
data = s.recv(1024)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_ota_req(client_connection, msg_ota_update_req):
|
def test_ota_req(ClientConnection, MsgOtaUpdateReq):
|
||||||
s = client_connection
|
s = ClientConnection
|
||||||
try:
|
try:
|
||||||
s.sendall(msg_ota_update_req)
|
s.sendall(MsgOtaUpdateReq)
|
||||||
_ = s.recv(1024)
|
data = s.recv(1024)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
import pytest, socket, time, os
|
import pytest, socket, time, os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
#from scapy.all import *
|
||||||
|
#from scapy.layers.inet import IP, TCP, TCP_client
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
SOLARMAN_SNR = os.getenv('SOLARMAN_SNR', '00000080')
|
SOLARMAN_SNR = os.getenv('SOLARMAN_SNR', '00000080')
|
||||||
@@ -108,7 +111,10 @@ def MsgInvalidInfo(): # Contact Info message wrong start byte
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def ClientConnection():
|
def ClientConnection():
|
||||||
|
#host = '172.16.30.7'
|
||||||
host = 'logger.talent-monitoring.com'
|
host = 'logger.talent-monitoring.com'
|
||||||
|
#host = 'iot.talent-monitoring.com'
|
||||||
|
#host = '127.0.0.1'
|
||||||
port = 10000
|
port = 10000
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
s.connect((host, port))
|
s.connect((host, port))
|
||||||
@@ -125,7 +131,10 @@ def checkResponse(data, Msg):
|
|||||||
|
|
||||||
|
|
||||||
def tempClientConnection():
|
def tempClientConnection():
|
||||||
|
#host = '172.16.30.7'
|
||||||
host = 'logger.talent-monitoring.com'
|
host = 'logger.talent-monitoring.com'
|
||||||
|
#host = 'iot.talent-monitoring.com'
|
||||||
|
#host = '127.0.0.1'
|
||||||
port = 10000
|
port = 10000
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
s.connect((host, port))
|
s.connect((host, port))
|
||||||
@@ -136,10 +145,11 @@ def tempClientConnection():
|
|||||||
|
|
||||||
def test_open_close():
|
def test_open_close():
|
||||||
try:
|
try:
|
||||||
for _ in tempClientConnection():
|
for s in tempClientConnection():
|
||||||
pass # test generator tempClientConnection()
|
pass
|
||||||
except:
|
except:
|
||||||
assert False
|
assert False
|
||||||
|
assert True
|
||||||
|
|
||||||
def test_conn_msg(ClientConnection,MsgContactInfo, MsgContactResp):
|
def test_conn_msg(ClientConnection,MsgContactInfo, MsgContactResp):
|
||||||
s = ClientConnection
|
s = ClientConnection
|
||||||
|
|||||||
Reference in New Issue
Block a user