diff --git a/.coveragerc b/.coveragerc
index 6b08179..890dd1b 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,2 +1,3 @@
[run]
-branch = True
\ No newline at end of file
+branch = True
+relative_files = True
\ No newline at end of file
diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
index f51ae3d..9061986 100644
--- a/.github/workflows/python-app.yml
+++ b/.github/workflows/python-app.yml
@@ -18,10 +18,11 @@ on:
- '**.dockerfile' # Do no build on *.dockerfile changes
- '**.sh' # Do no build on *.sh changes
pull_request:
- branches: [ "main" ]
+ branches: [ "main", "dev-*" ]
permissions:
contents: read
+ pull-requests: read # allows SonarCloud to decorate PRs with analysis results
jobs:
build:
@@ -29,7 +30,15 @@ jobs:
runs-on: ubuntu-latest
steps:
+ - name: Set timezone
+ uses: szenius/set-timezone@v2.0
+ with:
+ timezoneLinux: "Europe/Berlin"
+ timezoneMacos: "Europe/Berlin"
+ timezoneWindows: "Europe/Berlin"
- uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Fetch all history for all tags and branches
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
@@ -47,4 +56,19 @@ jobs:
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
- python -m pytest app
+ pip install pytest pytest-cov
+ #pytest app --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html
+ python -m pytest app --cov=app/src --cov-report=xml
+ - name: Analyze with SonarCloud
+ uses: SonarSource/sonarcloud-github-action@v2.2.0
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ with:
+ projectBaseDir: .
+ args:
+ -Dsonar.projectKey=s-allius_tsun-gen3-proxy
+ -Dsonar.organization=s-allius
+ -Dsonar.python.version=3.12
+ -Dsonar.python.coverage.reportPaths=coverage.xml
+ -Dsonar.tests=system_tests,app/tests
+ -Dsonar.source=app/src
diff --git a/.sonarlint/connectedMode.json b/.sonarlint/connectedMode.json
new file mode 100644
index 0000000..4a9cc82
--- /dev/null
+++ b/.sonarlint/connectedMode.json
@@ -0,0 +1,4 @@
+{
+ "sonarCloudOrganization": "s-allius",
+ "projectKey": "s-allius_tsun-gen3-proxy"
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 42fb2c2..dd2d0cf 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -11,5 +11,9 @@
"python.testing.pytestEnabled": true,
"flake8.args": [
"--extend-exclude=app/tests/*.py system_tests/*.py"
- ]
+ ],
+ "sonarlint.connectedMode.project": {
+ "connectionId": "s-allius",
+ "projectKey": "s-allius_tsun-gen3-proxy"
+ }
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 934a71e..ffe1250 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [unreleased]
+## [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)
+- improve the message handling, to avoid hangs
+- GEN3: allow long timeouts until we received first inverter data (not only device data)
+- bump aiomqtt to version 2.2.0
+- bump schema to version 0.7.7
+- Home Assistant: improve inverter status value texts
+- GEN3: add inverter status
+- fix flapping registers [#128](https://github.com/s-allius/tsun-gen3-proxy/issues/128)
+- register OUTPUT_COEFFICIENT at HA
+- GEN3: INVERTER_STATUS,
+- add config option to disable the MODBUS polling [#120](https://github.com/s-allius/tsun-gen3-proxy/issues/120)
+- make the maximum output coefficient configurable [#123](https://github.com/s-allius/tsun-gen3-proxy/issues/123)
+- cleanup shutdown
+- add preview build
+- MODBUS: the last digit of the inverter version is a hexadecimal number [#119](https://github.com/s-allius/tsun-gen3-proxy/issues/119)
+- GEN3PLUS: add client_mode connection on port 8899 [#117](https://github.com/s-allius/tsun-gen3-proxy/issues/117)
+
## [0.9.0] - 2024-07-01
- fix exception in MODBUS timeout callback
diff --git a/README.md b/README.md
index 67a6ead..1ec2294 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
-
+
@@ -165,6 +165,9 @@ 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
node_id = 'inv_3' # MQTT replacement for inverters serial number
suggested_area = 'garage' # suggested installation place for home-assistant
+# if your inverter supports SSL connections you must use the client_mode. Pls, uncomment
+# the next line and configure the fixed IP of your inverter
+#client_mode = {host = '192.168.0.1', port = 8899}
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
@@ -188,8 +191,20 @@ The standard web interface of the inverter can be accessed at `http:///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.
+❗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 access to the web interface does not work, it can also be redirected via DNS redirection, as is necessary for the GEN3 inverters.
+## Client Mode (GEN3PLUS only)
+
+Newer GEN3PLUS inverters support SSL encrypted connections over port 10443 to the TSUN cloud. In this case you can't loop the proxy into this connection, since the certicate verification of the inverter don't allow this. You can configure the proxy in client-mode to establish an unencrypted connection to the inverter. For this porpuse the inverter listen on port `8899`.
+
+There are some requirements to be met:
+
+- the inverter should have a fixed IP
+- the proxy must be able to reach the inverter. You must configure a corresponding route in your router if the inverter and the proxy are in different IP networks
+- add a 'client_mode' line to your config.toml file, to specify the inverter's ip address
+
## DNS Settings
### Loop the proxy into the connection
@@ -226,11 +241,11 @@ In the following table you will find an overview of which inverter model has bee
A combination with a red question mark should work, but I have not checked it in detail.
- Micro Inverter Model Fw. 1.00.06 Fw. 1.00.17 Fw. 1.00.20 Fw. 4.0.10
- GEN3 micro inverters (single MPPT): MS300, MS350, MS400 MS400-D ❓ ❓ ❓ ➖
- GEN3 micro inverters (dual MPPT): MS600, MS700, MS800 MS600-D, MS800-D ✔️ ✔️ ✔️ ➖
- GEN3 PLUS micro inverters: MS1600, MS1800, MS2000 MS2000-D ➖ ➖ ➖ ✔️
- TITAN micro inverters: TSOL-MP3000, MP2250, MS3000 ❓ ❓ ❓ ❓
+ Micro Inverter Model Fw. 1.00.06 Fw. 1.00.17 Fw. 1.00.20 Fw. 4.0.10 Fw. 4.0.20
+ GEN3 micro inverters (single MPPT): MS300, MS350, MS400 MS400-D ❓ ❓ ❓ ➖ ➖
+ GEN3 micro inverters (dual MPPT): MS600, MS700, MS800 MS600-D, MS800-D ✔️ ✔️ ✔️ ➖ ➖
+ GEN3 PLUS micro inverters: MS1600, MS1800, MS2000 MS2000-D ➖ ➖ ➖ ✔️ ✔️
+ TITAN micro inverters: TSOL-MP3000, MP2250, MS3000 ❓ ❓ ❓ ❓ ❓
```txt
diff --git a/app/build.sh b/app/build.sh
index dbb1f86..cf9e8b0 100755
--- a/app/build.sh
+++ b/app/build.sh
@@ -17,39 +17,56 @@ VERSION="${VERSION:1}"
arr=(${VERSION//./ })
MAJOR=${arr[0]}
IMAGE=tsun-gen3-proxy
+GREEN='\033[0;32m'
+BLUE='\033[0;34m'
+NC='\033[0m'
if [[ $1 == debug ]] || [[ $1 == dev ]] ;then
IMAGE=docker.io/sallius/${IMAGE}
-VERSION=${VERSION}-$1
-elif [[ $1 == rc ]] || [[ $1 == rel ]];then
+VERSION=${VERSION}+$1
+elif [[ $1 == rc ]] || [[ $1 == rel ]] || [[ $1 == preview ]] ;then
IMAGE=ghcr.io/s-allius/${IMAGE}
else
echo argument missing!
-echo try: $0 '[debug|dev|rc|rel]'
+echo try: $0 '[debug|dev|preview|rc|rel]'
exit 1
fi
-echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE
-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
-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
-
-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
-docker push -q ghcr.io/s-allius/tsun-gen3-proxy:rc
-docker push -q ghcr.io/s-allius/tsun-gen3-proxy:${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
-docker push -q ghcr.io/s-allius/tsun-gen3-proxy:latest
-docker push -q ghcr.io/s-allius/tsun-gen3-proxy:${MAJOR}
-docker push -q ghcr.io/s-allius/tsun-gen3-proxy:${VERSION}
+if [[ $1 == debug ]] ;then
+BUILD_ENV="dev"
+else
+BUILD_ENV="production"
fi
-echo 'check docker-compose.yaml file'
-docker-compose config -q
\ No newline at end of file
+BUILD_CMD="buildx build --push --build-arg \"VERSION=${VERSION}\" --build-arg \"environment=${BUILD_ENV}\" --attest type=provenance,mode=max --attest type=sbom,generator=docker/scout-sbom-indexer:latest"
+ARCH="--platform linux/amd64,linux/arm64,linux/arm/v7"
+LABELS="--label \"org.opencontainers.image.created=${BUILD_DATE}\" --label \"org.opencontainers.image.version=${VERSION}\" --label \"org.opencontainers.image.revision=${BRANCH}\""
+
+echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE
+if [[ $1 == debug ]];then
+docker ${BUILD_CMD} ${ARCH} ${LABELS} --build-arg "LOG_LVL=DEBUG" -t ${IMAGE}:debug app
+
+elif [[ $1 == dev ]];then
+docker ${BUILD_CMD} ${ARCH} ${LABELS} -t ${IMAGE}:dev app
+
+elif [[ $1 == preview ]];then
+echo 'login to ghcr.io'
+echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
+docker ${BUILD_CMD} ${ARCH} ${LABELS} -t ${IMAGE}:preview -t ${IMAGE}:${VERSION} app
+
+elif [[ $1 == rc ]];then
+echo 'login to ghcr.io'
+echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
+docker ${BUILD_CMD} ${ARCH} ${LABELS} -t ${IMAGE}:rc -t ${IMAGE}:${VERSION} app
+
+elif [[ $1 == rel ]];then
+echo 'login to ghcr.io'
+echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
+docker ${BUILD_CMD} ${ARCH} ${LABELS} --no-cache -t ${IMAGE}:latest -t ${IMAGE}:${MAJOR} -t ${IMAGE}:${VERSION} app
+fi
+
+echo -e "${BLUE} => checking docker-compose.yaml file${NC}"
+docker-compose config -q
+echo
+echo -e "${GREEN}${BUILD_DATE} => Version: ${VERSION}${NC} finished"
+echo
diff --git a/app/config/default_config.toml b/app/config/default_config.toml
index fbe2651..744835a 100644
--- a/app/config/default_config.toml
+++ b/app/config/default_config.toml
@@ -31,12 +31,14 @@ inverters.allow_all = true # allow inverters, even if we have no inverter mapp
[inverters."R170000000000001"]
#node_id = '' # Optional, MQTT replacement for inverters serial number
#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
#pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
#[inverters."R17xxxxxxxxxxxx2"]
#node_id = '' # Optional, MQTT replacement for inverters serial number
#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
#pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
@@ -44,6 +46,12 @@ inverters.allow_all = true # allow inverters, even if we have no inverter mapp
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
#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
+# the next line and configure the fixed IP of your inverter
+#client_mode = {host = '192.168.0.1', port = 8899}
+
#pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
#pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
#pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
diff --git a/app/proxy.svg b/app/proxy.svg
index b111fd4..32caab7 100644
--- a/app/proxy.svg
+++ b/app/proxy.svg
@@ -4,381 +4,408 @@
-
-
+
+
G
-
+
A0
-
-
-
-You can stick notes
-on diagrams too!
+
+
+
+You can stick notes
+on diagrams too!
A1
-
-Singleton
+
+Singleton
A2
-
-Mqtt
-
-<static>ha_restarts
-<static>__client
-<static>__cb_MqttIsUp
-
-<async>publish()
-<async>close()
+
+Mqtt
+
+<static>ha_restarts
+<static>__client
+<static>__cb_MqttIsUp
+
+<async>publish()
+<async>close()
A1->A2
-
-
+
+
A11
-
-Inverter
-
-cls.db_stat
-cls.entity_prfx
-cls.discovery_prfx
-cls.proxy_node_id
-cls.proxy_unique_id
-cls.mqtt:Mqtt
-
+
+Inverter
+
+cls.db_stat
+cls.entity_prfx
+cls.discovery_prfx
+cls.proxy_node_id
+cls.proxy_unique_id
+cls.mqtt:Mqtt
+
A2->A11
-
+
A3
-
-Modbus
-
-que
-snd_handler
-rsp_handler
-timeout:max_retires
-last_xxx
-err
-retry_cnt
-req_pend
-tim
-
-build_msg()
-recv_req()
-recv_resp()
+
+Modbus
+
+que
+snd_handler
+rsp_handler
+timeout
+max_retires
+last_xxx
+err
+retry_cnt
+req_pend
+tim
+
+build_msg()
+recv_req()
+recv_resp()
+close()
A4
-
-IterRegistry
-
-
-__iter__
+
+IterRegistry
+
+
+__iter__
A5
-
-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
+
+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
A4->A5
-
-
+
+
A6
-
-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()
+
+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()
A5->A6
-
-
+
+
A7
-
-SolarmanV5
-
-control
-serial
-snr
-db:InfosG3P
-mb:Modbus
-switch
-
-msg_unknown()
-close()
+
+SolarmanV5
+
+control
+serial
+snr
+db:InfosG3P
+mb:Modbus
+switch
+
+msg_unknown()
+close()
A5->A7
-
-
+
+
A6->A3
-
-
-1
-has
+
+
+1
+has
A8
-
-ConnectionG3
-
-remoteStream:ConnectionG3
-
-close()
+
+ConnectionG3
+
+remote_stream:ConnectionG3
+
+healthy()
+close()
A6->A8
-
-
+
+
A7->A3
-
-
-1
-has
+
+
+1
+has
A9
-
-ConnectionG3P
-
-remoteStream:ConnectionG3P
-
-close()
+
+ConnectionG3P
+
+remote_stream:ConnectionG3P
+
+healthy()
+close()
A7->A9
-
-
+
+
A8->A8
-
-
-0..1
-has
+
+
+0..1
+has
A12
-
-InverterG3
-
-__ha_restarts
-
-async_create_remote()
-close()
+
+InverterG3
+
+__ha_restarts
+
+async_create_remote()
+async_publ_mqtt()
+close()
A8->A12
-
-
+
+
A9->A9
-
-
-0..1
-has
+
+
+0..1
+has
A13
-
-InverterG3P
-
-__ha_restarts
-
-async_create_remote()
-close()
+
+InverterG3P
+
+__ha_restarts
+
+async_create_remote(
+)async_publ_mqtt()
+close()
A9->A13
-
-
+
+
A10
-
-AsyncStream
-
-reader
-writer
-addr
-r_addr
-l_addr
-
-<async>server_loop()
-<async>client_loop()
-<async>loop
-disc()
-close()
-__async_read()
-async_write()
-__async_forward()
+
+AsyncStream
+
+reader
+writer
+addr
+r_addr
+l_addr
+
+<async>server_loop()
+<async>client_loop()
+<async>loop
+disc()
+close()
+__async_read()
+async_write()
+__async_forward()
A10->A8
-
-
+
+
A10->A9
-
-
+
+
A11->A12
-
-
+
+
A11->A13
-
-
+
+
A14
-
-Infos
-
-stat
-new_stat_data
-info_dev
-
-static_init()
-dev_value()
-inc_counter()
-dec_counter()
-ha_proxy_conf
-ha_conf
-update_db
-set_db_def_value
-get_db_value
-ignore_this_device
+
+Infos
+
+stat
+new_stat_data
+info_dev
+
+static_init()
+dev_value()
+inc_counter()
+dec_counter()
+ha_proxy_conf
+ha_conf
+ha_remove
+update_db
+set_db_def_value
+get_db_value
+ignore_this_device
A15
-
-InfosG3
-
-
-ha_confs()
-parse()
+
+InfosG3
+
+
+ha_confs()
+parse()
A14->A15
-
-
+
+
A16
-
-InfosG3P
-
-
-ha_confs()
-parse()
+
+InfosG3P
+
+
+ha_confs()
+parse()
A14->A16
-
-
+
+
A15->A6
-
-
+
+
A16->A7
-
-
+
+
+
+
+
+A17
+
+ModbusConn
+
+host
+port
+addr
+stream:InverterG3P
+
+
+
+
+A17->A13
+
+
+1
+has
diff --git a/app/proxy.yuml b/app/proxy.yuml
index 60b506e..c629d35 100644
--- a/app/proxy.yuml
+++ b/app/proxy.yuml
@@ -4,25 +4,27 @@
[note: You can stick notes on diagrams too!{bg:cornsilk}]
[Singleton]^[Mqtt|ha_restarts;__client;__cb_MqttIsUp|publish();close()]
-[Modbus|que;;snd_handler;rsp_handler;timeout:max_retires;last_xxx;err;retry_cnt;req_pend;tim|build_msg();recv_req();recv_resp()]
+[Modbus|que;;snd_handler;rsp_handler;timeout;max_retires;last_xxx;err;retry_cnt;req_pend;tim|build_msg();recv_req();recv_resp();close()]
[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;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]^[SolarmanV5|control;serial;snr;db:InfosG3P;mb:Modbus;switch|msg_unknown();;close()]
-[Talent]^[ConnectionG3|remoteStream:ConnectionG3|close()]
+[Talent]^[ConnectionG3|remote_stream:ConnectionG3|healthy();close()]
[Talent]has-1>[Modbus]
-[SolarmanV5]^[ConnectionG3P|remoteStream:ConnectionG3P|close()]
+[SolarmanV5]^[ConnectionG3P|remote_stream:ConnectionG3P|healthy();close()]
[SolarmanV5]has-1>[Modbus]
[AsyncStream|reader;writer;addr;r_addr;l_addr|server_loop();client_loop();loop;disc();close();;__async_read();async_write();__async_forward()]^[ConnectionG3]
[AsyncStream]^[ConnectionG3P]
-[Inverter|cls.db_stat;cls.entity_prfx;cls.discovery_prfx;cls.proxy_node_id;cls.proxy_unique_id;cls.mqtt:Mqtt|]^[InverterG3|__ha_restarts|async_create_remote();;close()]
-[Inverter]^[InverterG3P|__ha_restarts|async_create_remote();;close()]
+[Inverter|cls.db_stat;cls.entity_prfx;cls.discovery_prfx;cls.proxy_node_id;cls.proxy_unique_id;cls.mqtt:Mqtt|]^[InverterG3|__ha_restarts|async_create_remote();async_publ_mqtt();;close()]
+[Inverter]^[InverterG3P|__ha_restarts|async_create_remote(;)async_publ_mqtt();close()]
[Mqtt]-[Inverter]
[ConnectionG3]^[InverterG3]
[ConnectionG3]has-0..1>[ConnectionG3]
[ConnectionG3P]^[InverterG3P]
[ConnectionG3P]has-0..1>[ConnectionG3P]
-[Infos|stat;new_stat_data;info_dev|static_init();dev_value();inc_counter();dec_counter();ha_proxy_conf;ha_conf;update_db;set_db_def_value;get_db_value;ignore_this_device]^[InfosG3||ha_confs();parse()]
+
+[Infos|stat;new_stat_data;info_dev|static_init();dev_value();inc_counter();dec_counter();ha_proxy_conf;ha_conf;ha_remove;update_db;set_db_def_value;get_db_value;ignore_this_device]^[InfosG3||ha_confs();parse()]
[Infos]^[InfosG3P||ha_confs();parse()]
[InfosG3P]->[SolarmanV5]
[InfosG3]->[Talent]
+[ModbusConn|host;port;addr;stream:InverterG3P;|]has-1>[InverterG3P]
diff --git a/app/requirements.txt b/app/requirements.txt
index ed9dcb0..2aa2067 100644
--- a/app/requirements.txt
+++ b/app/requirements.txt
@@ -1,4 +1,4 @@
- aiomqtt==2.0.1
- schema==0.7.5
+ aiomqtt==2.2.0
+ schema==0.7.7
aiocron==1.8
- aiohttp==3.9.5
\ No newline at end of file
+ aiohttp==3.10.2
\ No newline at end of file
diff --git a/app/src/async_stream.py b/app/src/async_stream.py
index f4b0ff9..f6b58b5 100644
--- a/app/src/async_stream.py
+++ b/app/src/async_stream.py
@@ -17,10 +17,10 @@ class AsyncStream():
'''maximum processing time for a received msg in sec'''
MAX_START_TIME = 400
'''maximum time without a received msg in sec'''
- MAX_INV_IDLE_TIME = 90
+ MAX_INV_IDLE_TIME = 120
'''maximum time without a received msg from the inverter in sec'''
- MAX_CLOUD_IDLE_TIME = 360
- '''maximum time without a received msg from cloud side in sec'''
+ MAX_DEF_IDLE_TIME = 360
+ '''maximum default time without a received msg in sec'''
def __init__(self, reader: StreamReader, writer: StreamWriter,
addr) -> None:
@@ -35,43 +35,50 @@ class AsyncStream():
self.proc_max = 0
def __timeout(self) -> int:
- if self.state == State.init:
+ if self.state == State.init or self.state == State.received:
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:
- if self.server_side:
- to = self.MAX_INV_IDLE_TIME
- else:
- to = self.MAX_CLOUD_IDLE_TIME
+ to = self.MAX_DEF_IDLE_TIME
return to
+ async def publish_outstanding_mqtt(self):
+ '''Publish all outstanding MQTT topics'''
+ try:
+ if self.unique_id:
+ await self.async_publ_mqtt()
+ await self._async_publ_mqtt_proxy_stat('proxy')
+ except Exception:
+ pass
+
async def server_loop(self, addr: str) -> None:
'''Loop for receiving messages from the inverter (server-side)'''
logger.info(f'[{self.node_id}:{self.conn_no}] '
f'Accept connection from {addr}')
self.inc_counter('Inverter_Cnt')
+ await self.publish_outstanding_mqtt()
await self.loop()
self.dec_counter('Inverter_Cnt')
+ await self.publish_outstanding_mqtt()
logger.info(f'[{self.node_id}:{self.conn_no}] Server loop stopped for'
f' r{self.r_addr}')
# if the server connection closes, we also have to disconnect
# the connection to te TSUN cloud
- if self.remoteStream:
+ if self.remote_stream:
logger.info(f'[{self.node_id}:{self.conn_no}] disc client '
- f'connection: [{self.remoteStream.node_id}:'
- f'{self.remoteStream.conn_no}]')
- await self.remoteStream.disc()
- try:
- await self._async_publ_mqtt_proxy_stat('proxy')
- except Exception:
- pass
+ f'connection: [{self.remote_stream.node_id}:'
+ f'{self.remote_stream.conn_no}]')
+ await self.remote_stream.disc()
- async def client_loop(self, addr: str) -> None:
+ async def client_loop(self, _: str) -> None:
'''Loop for receiving messages from the TSUN cloud (client-side)'''
- clientStream = await self.remoteStream.loop()
- logger.info(f'[{clientStream.node_id}:{clientStream.conn_no}] '
+ client_stream = await self.remote_stream.loop()
+ logger.info(f'[{client_stream.node_id}:{client_stream.conn_no}] '
'Client loop stopped for'
- f' l{clientStream.l_addr}')
+ f' l{client_stream.l_addr}')
# if the client connection closes, we don't touch the server
# connection. Instead we erase the client connection stream,
@@ -79,13 +86,13 @@ class AsyncStream():
# establish a new connection to the TSUN cloud
# erase backlink to inverter
- clientStream.remoteStream = None
+ client_stream.remote_stream = None
- if self.remoteStream == clientStream:
- # logging.debug(f'Client l{clientStream.l_addr} refs:'
- # f' {gc.get_referrers(clientStream)}')
+ if self.remote_stream == client_stream:
+ # logging.debug(f'Client l{client_stream.l_addr} refs:'
+ # f' {gc.get_referrers(client_stream)}')
# than erase client connection
- self.remoteStream = None
+ self.remote_stream = None
async def loop(self) -> Self:
"""Async loop handler for precessing all received messages"""
@@ -196,35 +203,35 @@ class AsyncStream():
if not self._forward_buffer:
return
try:
- if not self.remoteStream:
+ if not self.remote_stream:
await self.async_create_remote()
- if self.remoteStream:
- if self.remoteStream._init_new_client_conn():
- await self.remoteStream.async_write()
+ if self.remote_stream:
+ if self.remote_stream._init_new_client_conn():
+ await self.remote_stream.async_write()
- if self.remoteStream:
- self.remoteStream._update_header(self._forward_buffer)
+ if self.remote_stream:
+ self.remote_stream._update_header(self._forward_buffer)
hex_dump_memory(logging.INFO,
- f'Forward to {self.remoteStream.addr}:',
+ f'Forward to {self.remote_stream.addr}:',
self._forward_buffer,
len(self._forward_buffer))
- self.remoteStream.writer.write(self._forward_buffer)
- await self.remoteStream.writer.drain()
+ self.remote_stream.writer.write(self._forward_buffer)
+ await self.remote_stream.writer.drain()
self._forward_buffer = bytearray(0)
except OSError as error:
- if self.remoteStream:
- rmt = self.remoteStream
- self.remoteStream = None
+ if self.remote_stream:
+ rmt = self.remote_stream
+ self.remote_stream = None
logger.error(f'[{rmt.node_id}:{rmt.conn_no}] Fwd: {error} for '
f'l{rmt.l_addr} | r{rmt.r_addr}')
await rmt.disc()
rmt.close()
except RuntimeError as error:
- if self.remoteStream:
- rmt = self.remoteStream
- self.remoteStream = None
+ if self.remote_stream:
+ rmt = self.remote_stream
+ self.remote_stream = None
logger.info(f'[{rmt.node_id}:{rmt.conn_no}] '
f'Fwd: {error} for {rmt.l_addr}')
await rmt.disc()
diff --git a/app/src/config.py b/app/src/config.py
index 8121c86..6f16049 100644
--- a/app/src/config.py
+++ b/app/src/config.py
@@ -53,7 +53,12 @@ class Config():
Use(lambda s: s + '/'
if len(s) > 0 and
s[-1] != '/' else s)),
-
+ Optional('client_mode'): {
+ 'host': Use(str),
+ Optional('port', default=8899):
+ And(Use(int), lambda n: 1024 <= n <= 65535)
+ },
+ Optional('modbus_polling', default=True): Use(bool),
Optional('suggested_area', default=""): Use(str),
Optional('pv1'): {
Optional('type'): Use(str),
diff --git a/app/src/gen3/connection_g3.py b/app/src/gen3/connection_g3.py
index 7e7b96d..66683fa 100644
--- a/app/src/gen3/connection_g3.py
+++ b/app/src/gen3/connection_g3.py
@@ -1,5 +1,4 @@
import logging
-# import gc
from asyncio import StreamReader, StreamWriter
from async_stream import AsyncStream
from gen3.talent import Talent
@@ -15,7 +14,7 @@ class ConnectionG3(AsyncStream, Talent):
AsyncStream.__init__(self, reader, writer, addr)
Talent.__init__(self, server_side, id_str)
- self.remoteStream: 'ConnectionG3' = remote_stream
+ self.remote_stream: 'ConnectionG3' = remote_stream
'''
Our puplic methods
@@ -26,10 +25,10 @@ class ConnectionG3(AsyncStream, Talent):
# logger.info(f'AsyncStream refs: {gc.get_referrers(self)}')
async def async_create_remote(self) -> None:
- pass
+ pass # virtual interface
async def async_publ_mqtt(self) -> None:
- pass
+ pass # virtual interface
def healthy(self) -> bool:
logger.debug('ConnectionG3 healthy()')
diff --git a/app/src/gen3/infos_g3.py b/app/src/gen3/infos_g3.py
index 04e5c69..f20183a 100644
--- a/app/src/gen3/infos_g3.py
+++ b/app/src/gen3/infos_g3.py
@@ -31,6 +31,9 @@ class RegisterMap:
0xffffff06: Register.OTA_START_MSG,
0xffffff07: Register.SW_EXCEPTION,
0xffffff08: Register.MAX_DESIGNED_POWER,
+ 0xffffff09: Register.OUTPUT_COEFFICIENT,
+ 0xffffff0a: Register.INVERTER_STATUS,
+ 0xffffff0b: Register.POLLING_INTERVAL,
0xfffffffe: Register.TEST_REG1,
0xffffffff: Register.TEST_REG2,
0x00000640: Register.OUTPUT_POWER,
diff --git a/app/src/gen3/inverter_g3.py b/app/src/gen3/inverter_g3.py
index a20dc77..9e2a40a 100644
--- a/app/src/gen3/inverter_g3.py
+++ b/app/src/gen3/inverter_g3.py
@@ -9,9 +9,7 @@ from gen3.connection_g3 import ConnectionG3
from aiomqtt import MqttCodeError
from infos import Infos
-# import gc
-# logger = logging.getLogger('conn')
logger_mqtt = logging.getLogger('mqtt')
@@ -60,10 +58,10 @@ class InverterG3(Inverter, ConnectionG3):
logging.info(f'[{self.node_id}] Connect to {addr}')
connect = asyncio.open_connection(host, port)
reader, writer = await connect
- self.remoteStream = ConnectionG3(reader, writer, addr, self,
- False, self.id_str)
- logging.info(f'[{self.remoteStream.node_id}:'
- f'{self.remoteStream.conn_no}] '
+ self.remote_stream = ConnectionG3(reader, writer, addr, self,
+ False, self.id_str)
+ logging.info(f'[{self.remote_stream.node_id}:'
+ f'{self.remote_stream.conn_no}] '
f'Connected to {addr}')
asyncio.create_task(self.client_loop(addr))
diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py
index c6b0091..7cfcc64 100644
--- a/app/src/gen3/talent.py
+++ b/app/src/gen3/talent.py
@@ -1,7 +1,8 @@
import struct
import logging
-import time
+import pytz
from datetime import datetime
+from tzlocal import get_localzone
if __name__ == "app.src.gen3.talent":
from app.src.messages import hex_dump_memory, Message, State
@@ -9,12 +10,14 @@ if __name__ == "app.src.gen3.talent":
from app.src.my_timer import Timer
from app.src.config import Config
from app.src.gen3.infos_g3 import InfosG3
+ from app.src.infos import Register
else: # pragma: no cover
from messages import hex_dump_memory, Message, State
from modbus import Modbus
from my_timer import Timer
from config import Config
from gen3.infos_g3 import InfosG3
+ from infos import Register
logger = logging.getLogger('msg')
@@ -41,7 +44,7 @@ class Talent(Message):
MB_REGULAR_TIMEOUT = 60
def __init__(self, server_side: bool, id_str=b''):
- super().__init__(server_side, self.send_modbus_cb, mb_timeout=11)
+ super().__init__(server_side, self.send_modbus_cb, mb_timeout=15)
self.await_conn_resp_cnt = 0
self.id_str = id_str
self.contact_name = b''
@@ -71,12 +74,23 @@ class Talent(Message):
self.modbus_elms = 0 # for unit tests
self.node_id = 'G3' # will be overwritten in __set_serial_no
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
+ self.mb_timeout = self.MB_REGULAR_TIMEOUT
+ self.mb_start_timeout = self.MB_START_TIMEOUT
+ self.modbus_polling = False
'''
Our puplic methods
'''
def close(self) -> None:
logging.debug('Talent.close()')
+ if self.server_side:
+ # set inverter state to offline, if output power is very low
+ logging.debug('close power: '
+ f'{self.db.get_db_value(Register.OUTPUT_POWER, -1)}')
+ if self.db.get_db_value(Register.OUTPUT_POWER, 999) < 2:
+ self.db.set_db_def_value(Register.INVERTER_STATUS, 0)
+ self.new_data['env'] = True
+
# we have references to methods of this class in self.switch
# so we have to erase self.switch, otherwise this instance can't be
# deallocated by the garbage collector ==> we get a memory leak
@@ -98,6 +112,7 @@ class Talent(Message):
inv = inverters[serial_no]
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
self.db.set_pv_module_details(inv)
else:
@@ -113,41 +128,46 @@ class Talent(Message):
self.unique_id = serial_no
def read(self) -> float:
+ '''process all received messages in the _recv_buffer'''
self._read()
+ while True:
+ if not self.header_valid:
+ self.__parse_header(self._recv_buffer, len(self._recv_buffer))
- if not self.header_valid:
- self.__parse_header(self._recv_buffer, len(self._recv_buffer))
+ if self.header_valid and \
+ len(self._recv_buffer) >= (self.header_len + self.data_len):
+ if self.state == State.init:
+ self.state = State.received # received 1st package
- if self.header_valid and len(self._recv_buffer) >= (self.header_len +
- self.data_len):
- if self.state == State.init:
- self.state = State.received # received 1st package
+ log_lvl = self.log_lvl.get(self.msg_id, logging.WARNING)
+ if callable(log_lvl):
+ log_lvl = log_lvl()
- log_lvl = self.log_lvl.get(self.msg_id, logging.WARNING)
- if callable(log_lvl):
- log_lvl = log_lvl()
+ hex_dump_memory(log_lvl, f'Received from {self.addr}:'
+ f' BufLen: {len(self._recv_buffer)}'
+ f' HdrLen: {self.header_len}'
+ f' DtaLen: {self.data_len}',
+ self._recv_buffer, len(self._recv_buffer))
- hex_dump_memory(log_lvl, f'Received from {self.addr}:',
- self._recv_buffer, self.header_len+self.data_len)
+ self.__set_serial_no(self.id_str.decode("utf-8"))
+ self.__dispatch_msg()
+ self.__flush_recv_msg()
+ else:
+ return 0 # don not wait before sending a response
- self.__set_serial_no(self.id_str.decode("utf-8"))
- self.__dispatch_msg()
- self.__flush_recv_msg()
- return 0.5 # wait 500ms before sending a response
-
- def forward(self, buffer, buflen) -> None:
+ def forward(self) -> None:
+ '''add the actual receive msg to the forwarding queue'''
tsun = Config.get('tsun')
if tsun['enabled']:
- self._forward_buffer = buffer[:buflen]
+ buffer = self._recv_buffer
+ buflen = self.header_len+self.data_len
+ self._forward_buffer += buffer[:buflen]
hex_dump_memory(logging.DEBUG, 'Store for forwarding:',
buffer, buflen)
- self.__parse_header(self._forward_buffer,
- len(self._forward_buffer))
fnc = self.switch.get(self.msg_id, self.msg_unknown)
logger.info(self.__flow_str(self.server_side, 'forwrd') +
f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}')
- return
def send_modbus_cb(self, modbus_pdu: bytearray, log_lvl: int, state: str):
if self.state != State.up:
@@ -177,13 +197,13 @@ class Talent(Message):
self._send_modbus_cmd(func, addr, val, log_lvl)
def mb_timout_cb(self, exp_cnt):
- self.mb_timer.start(self.MB_REGULAR_TIMEOUT)
+ self.mb_timer.start(self.mb_timeout)
- if 0 == (exp_cnt % 30):
+ if 2 == (exp_cnt % 30):
# logging.info("Regular Modbus Status request")
- self._send_modbus_cmd(Modbus.READ_REGS, 0x2007, 2, logging.DEBUG)
+ self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.DEBUG)
else:
- self._send_modbus_cmd(Modbus.READ_REGS, 0x3008, 21, logging.DEBUG)
+ self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
def _init_new_client_conn(self) -> bool:
contact_name = self.contact_name
@@ -218,31 +238,43 @@ class Talent(Message):
return switch.get(type, '???')
def _timestamp(self): # pragma: no cover
- if False:
- # utc as epoche
- ts = time.time()
- else:
- # convert localtime in epoche
- ts = (datetime.now() - datetime(1970, 1, 1)).total_seconds()
+ '''returns timestamp fo the inverter as localtime
+ since 1.1.1970 in msec'''
+ # convert localtime in epoche
+ ts = (datetime.now() - datetime(1970, 1, 1)).total_seconds()
return round(ts*1000)
+ def _utcfromts(self, ts: float):
+ '''converts inverter timestamp into unix time (epoche)'''
+ dt = datetime.fromtimestamp(ts/1000, pytz.UTC). \
+ replace(tzinfo=get_localzone())
+ return dt.timestamp()
+
+ def _utc(self): # pragma: no cover
+ '''returns unix time (epoche)'''
+ return datetime.now().timestamp()
+
def _update_header(self, _forward_buffer):
'''update header for message before forwarding,
add time offset to timestamp'''
_len = len(_forward_buffer)
- result = struct.unpack_from('!lB', _forward_buffer, 0)
- id_len = result[1] # len of variable id string
- if _len < 2*id_len + 21:
- return
+ ofs = 0
+ while ofs < _len:
+ result = struct.unpack_from('!lB', _forward_buffer, 0)
+ msg_len = 4 + result[0]
+ id_len = result[1] # len of variable id string
+ if _len < 2*id_len + 21:
+ return
- result = struct.unpack_from('!B', _forward_buffer, id_len+6)
- msg_code = result[0]
- if msg_code == 0x71 or msg_code == 0x04:
- result = struct.unpack_from('!q', _forward_buffer, 13+2*id_len)
- ts = result[0] + self.ts_offset
- logger.debug(f'offset: {self.ts_offset:08x}'
- f' proxy-time: {ts:08x}')
- struct.pack_into('!q', _forward_buffer, 13+2*id_len, ts)
+ result = struct.unpack_from('!B', _forward_buffer, id_len+6)
+ msg_code = result[0]
+ if msg_code == 0x71 or msg_code == 0x04:
+ result = struct.unpack_from('!q', _forward_buffer, 13+2*id_len)
+ ts = result[0] + self.ts_offset
+ logger.debug(f'offset: {self.ts_offset:08x}'
+ f' proxy-time: {ts:08x}')
+ struct.pack_into('!q', _forward_buffer, 13+2*id_len, ts)
+ ofs += msg_len
# check if there is a complete header in the buffer, parse it
# and set
@@ -259,7 +291,7 @@ class Talent(Message):
if (buf_len < 5): # enough bytes to read len and id_len?
return
result = struct.unpack_from('!lB', buf, 0)
- len = result[0] # len of complete message
+ msg_len = result[0] # len of complete message
id_len = result[1] # len of variable id string
hdr_len = 5+id_len+2
@@ -273,10 +305,9 @@ class Talent(Message):
self.id_str = result[0]
self.ctrl = Control(result[1])
self.msg_id = result[2]
- self.data_len = len-id_len-3
+ self.data_len = msg_len-id_len-3
self.header_len = hdr_len
self.header_valid = True
- return
def __build_header(self, ctrl, msg_id=None) -> None:
if not msg_id:
@@ -321,12 +352,11 @@ class Talent(Message):
elif self.await_conn_resp_cnt > 0:
self.await_conn_resp_cnt -= 1
else:
- self.forward(self._recv_buffer, self.header_len+self.data_len)
- return
+ self.forward()
else:
logger.warning('Unknown Ctrl')
self.inc_counter('Unknown_Ctrl')
- self.forward(self._recv_buffer, self.header_len+self.data_len)
+ self.forward()
def __process_contact_info(self) -> bool:
result = struct.unpack_from('!B', self._recv_buffer, self.header_len)
@@ -348,8 +378,9 @@ class Talent(Message):
def msg_get_time(self):
if self.ctrl.is_ind():
if self.data_len == 0:
- self.state = State.pend # block MODBUS cmds
- self.mb_timer.start(self.MB_START_TIMEOUT)
+ if self.state == State.up:
+ self.state = State.pend # block MODBUS cmds
+
ts = self._timestamp()
logger.debug(f'time: {ts:08x}')
self.__build_header(0x91)
@@ -369,7 +400,7 @@ class Talent(Message):
logger.warning('Unknown Ctrl')
self.inc_counter('Unknown_Ctrl')
- self.forward(self._recv_buffer, self.header_len+self.data_len)
+ self.forward()
def parse_msg_header(self):
result = struct.unpack_from('!lB', self._recv_buffer, self.header_len)
@@ -383,11 +414,12 @@ class Talent(Message):
result = struct.unpack_from(f'!{id_len+1}pBq', self._recv_buffer,
self.header_len + 4)
+ timestamp = result[2]
logger.debug(f'ID: {result[0]} B: {result[1]}')
- logger.debug(f'time: {result[2]:08x}')
+ logger.debug(f'time: {timestamp:08x}')
# logger.info(f'time: {datetime.utcfromtimestamp(result[2]).strftime(
# "%Y-%m-%d %H:%M:%S")}')
- return msg_hdr_len
+ return msg_hdr_len, timestamp
def msg_collector_data(self):
if self.ctrl.is_ind():
@@ -402,7 +434,7 @@ class Talent(Message):
logger.warning('Unknown Ctrl')
self.inc_counter('Unknown_Ctrl')
- self.forward(self._recv_buffer, self.header_len+self.data_len)
+ self.forward()
def msg_inverter_data(self):
if self.ctrl.is_ind():
@@ -411,6 +443,10 @@ class Talent(Message):
self.__finish_send_msg()
self.__process_data()
self.state = State.up # allow 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)
elif self.ctrl.is_resp():
return # ignore received response
@@ -418,25 +454,26 @@ class Talent(Message):
logger.warning('Unknown Ctrl')
self.inc_counter('Unknown_Ctrl')
- self.forward(self._recv_buffer, self.header_len+self.data_len)
+ self.forward()
def __process_data(self):
- msg_hdr_len = self.parse_msg_header()
+ msg_hdr_len, ts = self.parse_msg_header()
for key, update in self.db.parse(self._recv_buffer, self.header_len
+ msg_hdr_len, self.node_id):
if update:
+ self._set_mqtt_timestamp(key, self._utcfromts(ts))
self.new_data[key] = True
def msg_ota_update(self):
if self.ctrl.is_req():
self.inc_counter('OTA_Start_Msg')
elif self.ctrl.is_ind():
- pass
+ pass # Ok, nothing to do
else:
logger.warning('Unknown Ctrl')
self.inc_counter('Unknown_Ctrl')
- self.forward(self._recv_buffer, self.header_len+self.data_len)
+ self.forward()
def parse_modbus_header(self):
@@ -445,27 +482,24 @@ class Talent(Message):
result = struct.unpack_from('!lBB', self._recv_buffer,
self.header_len)
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
def get_modbus_log_lvl(self) -> int:
if self.ctrl.is_req():
return logging.INFO
- elif self.ctrl.is_ind():
- if self.server_side:
- return self.mb.last_log_lvl
+ elif self.ctrl.is_ind() and self.server_side:
+ return self.mb.last_log_lvl
return logging.WARNING
def msg_modbus(self):
- hdr_len, modbus_len = self.parse_modbus_header()
+ hdr_len, _ = self.parse_modbus_header()
data = self._recv_buffer[self.header_len:
self.header_len+self.data_len]
if self.ctrl.is_req():
- if self.remoteStream.mb.recv_req(data[hdr_len:],
- self.remoteStream.
- msg_forward):
+ if self.remote_stream.mb.recv_req(data[hdr_len:],
+ self.remote_stream.
+ msg_forward):
self.inc_counter('Modbus_Command')
else:
self.inc_counter('Invalid_Msg_Format')
@@ -481,17 +515,18 @@ class Talent(Message):
hdr_len:],
self.node_id):
if update:
+ self._set_mqtt_timestamp(key, self._utc())
self.new_data[key] = True
self.modbus_elms += 1 # count for unit tests
else:
logger.warning('Unknown Ctrl')
self.inc_counter('Unknown_Ctrl')
- self.forward(self._recv_buffer, self.header_len+self.data_len)
+ self.forward()
def msg_forward(self):
- self.forward(self._recv_buffer, self.header_len+self.data_len)
+ self.forward()
def msg_unknown(self):
logger.warning(f"Unknow Msg: ID:{self.msg_id}")
self.inc_counter('Unknown_Msg')
- self.forward(self._recv_buffer, self.header_len+self.data_len)
+ self.forward()
diff --git a/app/src/gen3plus/connection_g3p.py b/app/src/gen3plus/connection_g3p.py
index 352ba5e..6591110 100644
--- a/app/src/gen3plus/connection_g3p.py
+++ b/app/src/gen3plus/connection_g3p.py
@@ -1,5 +1,4 @@
import logging
-# import gc
from asyncio import StreamReader, StreamWriter
from async_stream import AsyncStream
from gen3plus.solarman_v5 import SolarmanV5
@@ -11,11 +10,12 @@ class ConnectionG3P(AsyncStream, SolarmanV5):
def __init__(self, reader: StreamReader, writer: StreamWriter,
addr, remote_stream: 'ConnectionG3P',
- server_side: bool) -> None:
+ server_side: bool,
+ client_mode: bool) -> None:
AsyncStream.__init__(self, reader, writer, addr)
- SolarmanV5.__init__(self, server_side)
+ SolarmanV5.__init__(self, server_side, client_mode)
- self.remoteStream: 'ConnectionG3P' = remote_stream
+ self.remote_stream: 'ConnectionG3P' = remote_stream
'''
Our puplic methods
@@ -26,10 +26,10 @@ class ConnectionG3P(AsyncStream, SolarmanV5):
# logger.info(f'AsyncStream refs: {gc.get_referrers(self)}')
async def async_create_remote(self) -> None:
- pass
+ pass # virtual interface
async def async_publ_mqtt(self) -> None:
- pass
+ pass # virtual interface
def healthy(self) -> bool:
logger.debug('ConnectionG3P healthy()')
diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py
index bf0aed8..77655fd 100644
--- a/app/src/gen3plus/infos_g3p.py
+++ b/app/src/gen3plus/infos_g3p.py
@@ -3,9 +3,9 @@ import struct
from typing import Generator
if __name__ == "app.src.gen3plus.infos_g3p":
- from app.src.infos import Infos, Register
+ from app.src.infos import Infos, Register, ProxyMode
else: # pragma: no cover
- from infos import Infos, Register
+ from infos import Infos, Register, ProxyMode
class RegisterMap:
@@ -14,15 +14,15 @@ class RegisterMap:
__slots__ = ()
map = {
# 0x41020007: {'reg': Register.DEVICE_SNR, 'fmt': '>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501
@@ -56,19 +56,31 @@ class RegisterMap:
0x42010110: {'reg': Register.PV4_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
0x42010112: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501
0x42010126: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
- 0x42010170: {'reg': Register.NO_INPUTS, 'fmt': '!B'}, # noqa: E501
+ 0xffffff01: {'reg': Register.OUTPUT_COEFFICIENT},
+ 0xffffff02: {'reg': Register.POLLING_INTERVAL},
# 0x4281001c: {'reg': Register.POWER_ON_TIME, 'fmt': ' bool:
+ if 'dep' in row:
+ mode = row['dep']
+ if self.client_mode:
+ return mode != ProxyMode.CLIENT
+ else:
+ return mode != ProxyMode.SERVER
+ return False
def ha_confs(self, ha_prfx: str, node_id: str, snr: str,
sug_area: str = '') \
@@ -84,7 +96,10 @@ class InfosG3P(Infos):
# iterate over RegisterMap.map and get the register values
for row in RegisterMap.map.values():
info_id = row['reg']
- res = self.ha_conf(info_id, ha_prfx, node_id, snr, False, sug_area) # noqa: E501
+ if self.__hide_topic(row):
+ res = self.ha_remove(info_id, node_id, snr) # noqa: E501
+ else:
+ res = self.ha_conf(info_id, ha_prfx, node_id, snr, False, sug_area) # noqa: E501
if res:
yield res
diff --git a/app/src/gen3plus/inverter_g3p.py b/app/src/gen3plus/inverter_g3p.py
index 74c9b6b..6c1d6b5 100644
--- a/app/src/gen3plus/inverter_g3p.py
+++ b/app/src/gen3plus/inverter_g3p.py
@@ -9,9 +9,7 @@ from gen3plus.connection_g3p import ConnectionG3P
from aiomqtt import MqttCodeError
from infos import Infos
-# import gc
-# logger = logging.getLogger('conn')
logger_mqtt = logging.getLogger('mqtt')
@@ -45,8 +43,10 @@ class InverterG3P(Inverter, ConnectionG3P):
destroyed
'''
- def __init__(self, reader: StreamReader, writer: StreamWriter, addr):
- super().__init__(reader, writer, addr, None, True)
+ def __init__(self, reader: StreamReader, writer: StreamWriter, addr,
+ client_mode: bool = False):
+ super().__init__(reader, writer, addr, None,
+ server_side=True, client_mode=client_mode)
self.__ha_restarts = -1
async def async_create_remote(self) -> None:
@@ -60,10 +60,11 @@ class InverterG3P(Inverter, ConnectionG3P):
logging.info(f'[{self.node_id}] Connect to {addr}')
connect = asyncio.open_connection(host, port)
reader, writer = await connect
- self.remoteStream = ConnectionG3P(reader, writer, addr, self,
- False)
- logging.info(f'[{self.remoteStream.node_id}:'
- f'{self.remoteStream.conn_no}] '
+ self.remote_stream = ConnectionG3P(reader, writer, addr, self,
+ server_side=False,
+ client_mode=False)
+ logging.info(f'[{self.remote_stream.node_id}:'
+ f'{self.remote_stream.conn_no}] '
f'Connected to {addr}')
asyncio.create_task(self.client_loop(addr))
diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py
index 432ec2e..6ea06f0 100644
--- a/app/src/gen3plus/solarman_v5.py
+++ b/app/src/gen3plus/solarman_v5.py
@@ -1,5 +1,4 @@
import struct
-# import json
import logging
import time
import asyncio
@@ -19,7 +18,6 @@ else: # pragma: no cover
from my_timer import Timer
from gen3plus.infos_g3p import InfosG3P
from infos import Register
-# import traceback
logger = logging.getLogger('msg')
@@ -54,18 +52,24 @@ class SolarmanV5(Message):
AT_CMD = 1
MB_RTU_CMD = 2
MB_START_TIMEOUT = 40
+ '''start delay for Modbus polling in server mode'''
MB_REGULAR_TIMEOUT = 60
+ '''regular Modbus polling time in server mode'''
+ MB_CLIENT_DATA_UP = 30
+ '''Data up time in client mode'''
- def __init__(self, server_side: bool):
+ def __init__(self, server_side: bool, client_mode: bool):
super().__init__(server_side, self.send_modbus_cb, mb_timeout=5)
self.header_len = 11 # overwrite construcor in class Message
self.control = 0
self.seq = Sequence(server_side)
self.snr = 0
- self.db = InfosG3P()
+ self.db = InfosG3P(client_mode)
self.time_ofs = 0
self.forward_at_cmd_resp = False
+ self.no_forwarding = False
+ '''not allowed to connect to TSUN cloud by connection type'''
self.switch = {
0x4210: self.msg_data_ind, # real time data
@@ -128,12 +132,24 @@ class SolarmanV5(Message):
self.node_id = 'G3P' # will be overwritten in __set_serial_no
self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
+ self.mb_timeout = self.MB_REGULAR_TIMEOUT
+ self.mb_start_timeout = self.MB_START_TIMEOUT
+ '''timer value for next Modbus polling request'''
+ self.modbus_polling = False
'''
Our puplic methods
'''
def close(self) -> None:
logging.debug('Solarman.close()')
+ if self.server_side:
+ # set inverter state to offline, if output power is very low
+ logging.debug('close power: '
+ f'{self.db.get_db_value(Register.OUTPUT_POWER, -1)}')
+ if self.db.get_db_value(Register.OUTPUT_POWER, 999) < 2:
+ self.db.set_db_def_value(Register.INVERTER_STATUS, 0)
+ self.new_data['env'] = True
+
# we have references to methods of this class in self.switch
# so we have to erase self.switch, otherwise this instance can't be
# deallocated by the garbage collector ==> we get a memory leak
@@ -143,6 +159,31 @@ class SolarmanV5(Message):
self.mb_timer.close()
super().close()
+ async def send_start_cmd(self, snr: int, host: str,
+ start_timeout=MB_CLIENT_DATA_UP):
+ self.no_forwarding = True
+ self.snr = snr
+ self.__set_serial_no(snr)
+ self.mb_timeout = start_timeout
+ self.db.set_db_def_value(Register.IP_ADDRESS, host)
+ self.db.set_db_def_value(Register.POLLING_INTERVAL,
+ self.mb_timeout)
+ self.db.set_db_def_value(Register.HEARTBEAT_INTERVAL,
+ 120) # fixme
+ self.new_data['controller'] = True
+
+ self.state = State.up
+ self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
+ self.mb_timer.start(self.mb_timeout)
+
+ def new_state_up(self):
+ if self.state is not State.up:
+ self.state = State.up
+ if (self.modbus_polling):
+ self.mb_timer.start(self.mb_start_timeout)
+ self.db.set_db_def_value(Register.POLLING_INTERVAL,
+ self.mb_timeout)
+
def __set_serial_no(self, snr: int):
serial_no = str(snr)
if self.unique_id == serial_no:
@@ -159,6 +200,7 @@ class SolarmanV5(Message):
found = True
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
self.db.set_pv_module_details(inv)
@@ -175,50 +217,47 @@ class SolarmanV5(Message):
self.unique_id = serial_no
def read(self) -> float:
+ '''process all received messages in the _recv_buffer'''
self._read()
+ while True:
+ if not self.header_valid:
+ self.__parse_header(self._recv_buffer, len(self._recv_buffer))
- if not self.header_valid:
- self.__parse_header(self._recv_buffer, len(self._recv_buffer))
+ if self.header_valid and len(self._recv_buffer) >= \
+ (self.header_len + self.data_len+2):
+ 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
- if self.header_valid and len(self._recv_buffer) >= (self.header_len +
- self.data_len+2):
- 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()
- return 0 # wait 0s before sending a response
+ self.__set_serial_no(self.snr)
+ self.__dispatch_msg()
+ self.__flush_recv_msg()
+ else:
+ return 0 # wait 0s before sending a response
def forward(self, buffer, buflen) -> None:
+ '''add the actual receive msg to the forwarding queue'''
+ if self.no_forwarding:
+ return
tsun = Config.get('solarman')
if tsun['enabled']:
- self._forward_buffer = buffer[:buflen]
+ self._forward_buffer += buffer[:buflen]
hex_dump_memory(logging.DEBUG, 'Store for forwarding:',
buffer, buflen)
- self.__parse_header(self._forward_buffer,
- len(self._forward_buffer))
fnc = self.switch.get(self.control, self.msg_unknown)
logger.info(self.__flow_str(self.server_side, 'forwrd') +
f' Ctl: {int(self.control):#04x}'
f' Msg: {fnc.__name__!r}')
- return
def _init_new_client_conn(self) -> bool:
- # self.__build_header(0x91)
- # self._send_buffer += struct.pack(f'!{len(contact_name)+1}p'
- # f'{len(contact_mail)+1}p',
- # contact_name, contact_mail)
-
- # self.__finish_send_msg()
return False
'''
@@ -270,7 +309,6 @@ class SolarmanV5(Message):
self._recv_buffer = bytearray()
return
self.header_valid = True
- return
def __trailer_is_ok(self, buf: bytes, buf_len: int) -> bool:
crc = buf[self.data_len+11]
@@ -320,13 +358,17 @@ class SolarmanV5(Message):
'''update header for message before forwarding,
set sequence and checksum'''
_len = len(_forward_buffer)
- struct.pack_into(' None:
fnc = self.switch.get(self.control, self.msg_unknown)
@@ -378,27 +420,27 @@ class SolarmanV5(Message):
self._send_modbus_cmd(func, addr, val, log_lvl)
def mb_timout_cb(self, exp_cnt):
- self.mb_timer.start(self.MB_REGULAR_TIMEOUT)
+ self.mb_timer.start(self.mb_timeout)
- self._send_modbus_cmd(Modbus.READ_REGS, 0x3008, 21, logging.DEBUG)
+ self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
- if 0 == (exp_cnt % 30):
+ if 1 == (exp_cnt % 30):
# logging.info("Regular Modbus Status request")
- self._send_modbus_cmd(Modbus.READ_REGS, 0x2007, 2, logging.DEBUG)
+ self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 96, logging.DEBUG)
def at_cmd_forbidden(self, cmd: str, connection: str) -> bool:
return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \
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:
logger.warning(f'[{self.node_id}] ignore AT+ cmd,'
' as the state is not UP')
return
- AT_cmd = AT_cmd.strip()
+ at_cmd = at_cmd.strip()
- if self.at_cmd_forbidden(cmd=AT_cmd, connection='mqtt'):
- data_json = f'\'{AT_cmd}\' is forbidden'
+ if self.at_cmd_forbidden(cmd=at_cmd, connection='mqtt'):
+ data_json = f'\'{at_cmd}\' is forbidden'
node_id = self.node_id
key = 'at_resp'
logger.info(f'{key}: {data_json}')
@@ -407,8 +449,8 @@ class SolarmanV5(Message):
self.forward_at_cmd_resp = False
self.__build_header(0x4510)
- self._send_buffer += struct.pack(f'> 8
for key, update in self.db.parse(self._recv_buffer, msg_type, ftype,
@@ -443,6 +485,7 @@ class SolarmanV5(Message):
if update:
if key == 'inverter':
inv_update = True
+ self._set_mqtt_timestamp(key, ts)
self.new_data[key] = True
if inv_update:
@@ -459,16 +502,18 @@ class SolarmanV5(Message):
data = self._recv_buffer[self.header_len:]
result = struct.unpack_from(' Generator[tuple[str, dict], None, None]:
@@ -177,15 +187,29 @@ class Infos:
}
__comm_type_val_tpl = "{%set com_types = ['n/a','Wi-Fi', 'G4', 'G5', 'GPRS'] %}{{com_types[value_json['Communication_Type']|int(0)]|default(value_json['Communication_Type'])}}" # noqa: E501
- __status_type_val_tpl = "{%set inv_status = ['n/a', 'Online', 'Offline'] %}{{inv_status[value_json['Inverter_Status']|int(0)]|default(value_json['Inverter_Status'])}}" # noqa: E501
+ __status_type_val_tpl = "{%set inv_status = ['Off-line', 'On-grid', 'Off-grid'] %}{{inv_status[value_json['Inverter_Status']|int(0)]|default(value_json['Inverter_Status'])}}" # noqa: E501
+ __rated_power_val_tpl = "{% if 'Rated_Power' in value_json and value_json['Rated_Power'] != None %}{{value_json['Rated_Power']|string() +' W'}}{% else %}{{ this.state }}{% endif %}" # noqa: E501
+ __designed_power_val_tpl = '''
+{% if 'Max_Designed_Power' in value_json and
+ value_json['Max_Designed_Power'] != None %}
+ {% if value_json['Max_Designed_Power'] | int(0xffff) < 0x8000 %}
+ {{value_json['Max_Designed_Power']|string() +' W'}}
+ {% else %}
+ n/a
+ {% endif %}
+{% else %}
+ {{ this.state }}
+{% endif %}
+'''
+ __output_coef_val_tpl = "{% if 'Output_Coefficient' in value_json and value_json['Output_Coefficient'] != None %}{{value_json['Output_Coefficient']|string() +' %'}}{% else %}{{ this.state }}{% endif %}" # noqa: E501
__info_defs = {
# collector values used for device registration:
Register.COLLECTOR_FW_VERSION: {'name': ['collector', 'Collector_Fw_Version'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
- Register.CHIP_TYPE: {'name': ['collector', 'Chip_Type'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
- Register.CHIP_MODEL: {'name': ['collector', 'Chip_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
- Register.TRACE_URL: {'name': ['collector', 'Trace_URL'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
- Register.LOGGER_URL: {'name': ['collector', 'Logger_URL'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
+ Register.CHIP_TYPE: {'name': ['collector', 'Chip_Type'], 'singleton': False, 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
+ Register.CHIP_MODEL: {'name': ['collector', 'Chip_Model'], 'singleton': False, 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
+ Register.TRACE_URL: {'name': ['collector', 'Trace_URL'], 'singleton': False, 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
+ Register.LOGGER_URL: {'name': ['collector', 'Logger_URL'], 'singleton': False, 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
# inverter values used for device registration:
Register.PRODUCT_NAME: {'name': ['inverter', 'Product_Name'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
@@ -194,9 +218,9 @@ class Infos:
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.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_', 'fmt': '| string + " W"', '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_', 'fmt': '| string + " W"', 'name': 'Rated Power', 'icon': 'mdi:lightning-bolt', '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': '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': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # 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.PV2_MANUFACTURER: {'name': ['inverter', 'PV2_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
@@ -244,14 +268,16 @@ class Infos:
Register.EVENT_416: {'name': ['events', '416_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
# grid measures:
+ 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': '| 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': '| 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': '| 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': '| 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': '| 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:counter'}}, # 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:
+ 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': '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': '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
@@ -283,6 +309,7 @@ class Infos:
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 Generation', 'val_tpl': "{{ (value_json['pv6']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
# total:
+ 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': '| 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': '| float', 'name': 'Total Generation', 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501
@@ -295,6 +322,7 @@ class Infos:
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': '| 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': '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': '| string + " s"', 'name': 'Polling Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501
}
@property
@@ -305,7 +333,7 @@ class Infos:
def info_defs(self) -> dict:
return self.__info_defs
- def dev_value(self, idx: str | int) -> str | int | float | None:
+ def dev_value(self, idx: str | int) -> str | int | float | dict | None:
'''returns the stored device value from our database
idx:int ==> lookup the value in the database and return it as str,
@@ -318,29 +346,29 @@ class Infos:
elif idx in self.info_defs:
row = self.info_defs[idx]
if 'singleton' in row and row['singleton']:
- dict = self.stat
+ db_dict = self.stat
else:
- dict = self.db
+ db_dict = self.db
keys = row['name']
for key in keys:
- if key not in dict:
+ if key not in db_dict:
return None # value not found in the database
- dict = dict[key]
- return dict # value of the reqeusted entry
+ db_dict = db_dict[key]
+ return db_dict # value of the reqeusted entry
return None # unknwon idx, not in info_defs
def inc_counter(self, counter: str) -> None:
'''inc proxy statistic counter'''
- dict = self.stat['proxy']
- dict[counter] += 1
+ db_dict = self.stat['proxy']
+ db_dict[counter] += 1
def dec_counter(self, counter: str) -> None:
'''dec proxy statistic counter'''
- dict = self.stat['proxy']
- dict[counter] -= 1
+ db_dict = self.stat['proxy']
+ db_dict[counter] -= 1
def ha_proxy_confs(self, ha_prfx: str, node_id: str, snr: str) \
-> Generator[tuple[str, str, str, str], None, None]:
@@ -363,6 +391,20 @@ class Infos:
def ha_conf(self, key, ha_prfx, node_id, snr, singleton: bool,
sug_area: str = '') -> tuple[str, str, str, str] | None:
+ '''Method to build json register struct for home-assistant
+ auto configuration and the unique entity string, for all proxy
+ registers
+
+ arguments:
+ key ==> index of info_defs dict which reference the topic
+ ha_prfx:str ==> MQTT prefix for the home assistant 'stat_t string
+ node_id:str ==> node id of the inverter, used to build unique entity
+ snr:str ==> serial number of the inverter, used to build unique
+ entity strings
+ singleton ==> bool to allow/disaalow proxy topics which are common
+ for all invters
+ sug_area ==> area name for home assistant
+ '''
if key not in self.info_defs:
return None
row = self.info_defs[key]
@@ -466,7 +508,40 @@ class Infos:
return json.dumps(attr), component, node_id, attr['uniq_id']
return None
- def _key_obj(self, id: Register) -> list:
+ def ha_remove(self, key, node_id, snr) -> tuple[str, str, str, str] | None:
+ '''Method to build json unregister struct for home-assistant
+ to remove topics per auto configuration. Only for inverer topics.
+
+ arguments:
+ key ==> index of info_defs dict which reference the topic
+ node_id:str ==> node id of the inverter, used to build unique entity
+ snr:str ==> serial number of the inverter, used to build unique
+ entity strings
+
+ hint:
+ the returned tuple must have the same format as self.ha_conf()
+ '''
+ if key not in self.info_defs:
+ return None
+ row = self.info_defs[key]
+
+ if 'singleton' in row and row['singleton']:
+ return None
+
+ # check if we have details for home assistant
+ if 'ha' in row:
+ ha = row['ha']
+ if 'comp' in ha:
+ component = ha['comp']
+ else:
+ component = 'sensor'
+ attr = {}
+ uniq_id = ha['id']+snr
+
+ return json.dumps(attr), component, node_id, uniq_id
+ return None
+
+ def _key_obj(self, id: Register) -> tuple:
d = self.info_defs.get(id, {'name': None, 'level': logging.DEBUG,
'unit': ''})
if 'ha' in d and 'must_incr' in d['ha']:
@@ -478,21 +553,21 @@ class Infos:
def update_db(self, keys: list, must_incr: bool, result):
name = ''
- dict = self.db
+ db_dict = self.db
for key in keys[:-1]:
- if key not in dict:
- dict[key] = {}
- dict = dict[key]
+ if key not in db_dict:
+ db_dict[key] = {}
+ db_dict = db_dict[key]
name += key + '.'
- if keys[-1] not in dict:
+ if keys[-1] not in db_dict:
update = (not must_incr or result > 0)
else:
if must_incr:
- update = dict[keys[-1]] < result
+ update = db_dict[keys[-1]] < result
else:
- update = dict[keys[-1]] != result
+ update = db_dict[keys[-1]] != result
if update:
- dict[keys[-1]] = result
+ db_dict[keys[-1]] = result
name += keys[-1]
return name, update
@@ -546,13 +621,13 @@ class Infos:
return True
if 'gte' in dep:
- return not value >= dep['gte']
+ return value < dep['gte']
elif 'less_eq' in dep:
- return not value <= dep['less_eq']
+ return value > dep['less_eq']
return True
def set_pv_module_details(self, inv: dict) -> None:
- map = {'pv1': {'manufacturer': Register.PV1_MANUFACTURER, 'model': Register.PV1_MODEL}, # noqa: E501
+ pvs = {'pv1': {'manufacturer': Register.PV1_MANUFACTURER, 'model': Register.PV1_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
'pv4': {'manufacturer': Register.PV4_MANUFACTURER, 'model': Register.PV4_MODEL}, # noqa: E501
@@ -560,7 +635,7 @@ class Infos:
'pv6': {'manufacturer': Register.PV6_MANUFACTURER, 'model': Register.PV6_MODEL} # noqa: E501
}
- for key, reg in map.items():
+ for key, reg in pvs.items():
if key in inv:
if 'manufacturer' in inv[key]:
self.set_db_def_value(reg['manufacturer'],
diff --git a/app/src/messages.py b/app/src/messages.py
index bec2994..8170476 100644
--- a/app/src/messages.py
+++ b/app/src/messages.py
@@ -5,16 +5,16 @@ from enum import Enum
if __name__ == "app.src.messages":
- from app.src.infos import Infos
+ from app.src.infos import Infos, Register
from app.src.modbus import Modbus
else: # pragma: no cover
- from infos import Infos
+ from infos import Infos, Register
from modbus import Modbus
logger = logging.getLogger('msg')
-def hex_dump_memory(level, info, data, num):
+def hex_dump_memory(level, info, data, data_len):
n = 0
lines = []
lines.append(info)
@@ -22,20 +22,20 @@ def hex_dump_memory(level, info, data, num):
if not tracer.isEnabledFor(level):
return
- for i in range(0, num, 16):
+ for i in range(0, data_len, 16):
line = ' '
line += '%04x | ' % (i)
n += 16
for j in range(n-16, n):
- if j >= len(data):
+ if j >= data_len:
break
line += '%02x ' % abs(data[j])
line += ' ' * (3 * 16 + 9 - len(line)) + ' | '
for j in range(n-16, n):
- if j >= len(data):
+ if j >= data_len:
break
c = data[j] if not (data[j] < 0x20 or data[j] > 0x7e) else '.'
line += '%c' % c
@@ -91,6 +91,7 @@ class Message(metaclass=IterRegistry):
self._forward_buffer = bytearray(0)
self.new_data = {}
self.state = State.init
+ self.shutdown_started = False
'''
Empty methods, that have to be implemented in any child class which
@@ -102,7 +103,22 @@ class Message(metaclass=IterRegistry):
def _update_header(self, _forward_buffer):
'''callback for updating the header of the forward buffer'''
- return # pragma: no cover
+ pass # pragma: no cover
+
+ def _set_mqtt_timestamp(self, key, ts: float | None):
+ if key not in self.new_data or \
+ not self.new_data[key]:
+ if key == 'grid':
+ info_id = Register.TS_GRID
+ elif key == 'input':
+ info_id = Register.TS_INPUT
+ elif key == 'total':
+ info_id = Register.TS_TOTAL
+ else:
+ return
+ # tstr = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(ts))
+ # logger.info(f'update: key: {key} ts:{tstr}'
+ self.db.set_db_def_value(info_id, round(ts))
'''
Our puplic methods
@@ -111,7 +127,7 @@ class Message(metaclass=IterRegistry):
if self.mb:
self.mb.close()
self.mb = None
- pass # pragma: no cover
+ # pragma: no cover
def inc_counter(self, counter: str) -> None:
self.db.inc_counter(counter)
diff --git a/app/src/modbus.py b/app/src/modbus.py
index 7425b56..f7dbc27 100644
--- a/app/src/modbus.py
+++ b/app/src/modbus.py
@@ -41,8 +41,10 @@ class Modbus():
__crc_tab = []
map = {
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
- # 0x????: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
- 0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501
+ 0x202c: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
+
+ 0x3000: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
+ 0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf:1X}'"}, # noqa: E501
0x3009: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
0x300a: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
0x300b: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
@@ -104,6 +106,7 @@ class Modbus():
self.loop = asyncio.get_event_loop()
self.req_pend = False
self.tim = None
+ self.node_id = ''
def close(self):
"""free the queue and erase the callback handlers"""
@@ -111,7 +114,7 @@ class Modbus():
self.__stop_timer()
self.rsp_handler = None
self.snd_handler = None
- while not self.que.empty:
+ while not self.que.empty():
self.que.get_nowait()
def __del__(self):
@@ -178,6 +181,8 @@ class Modbus():
5: No MODBUS request pending
"""
# 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
return
@@ -265,7 +270,10 @@ class Modbus():
self.__start_timer()
self.snd_handler(self.last_req, self.last_log_lvl, state='Retrans')
else:
- logger.info(f'Modbus timeout {self}')
+ logger.info(f'[{self.node_id}] Modbus timeout '
+ f'(FCode: {self.last_fcode} '
+ f'Reg: 0x{self.last_reg:04x}, '
+ f'{self.last_len})')
self.counter['timeouts'] += 1
self.__send_next_from_que()
diff --git a/app/src/modbus_tcp.py b/app/src/modbus_tcp.py
new file mode 100644
index 0000000..b8ee1cf
--- /dev/null
+++ b/app/src/modbus_tcp.py
@@ -0,0 +1,76 @@
+import logging
+import traceback
+import asyncio
+from config import Config
+
+# import gc
+from gen3plus.inverter_g3p import InverterG3P
+
+logger = logging.getLogger('conn')
+
+
+class ModbusConn():
+ def __init__(self, host, port):
+ self.host = host
+ self.port = port
+ self.addr = (host, port)
+ self.stream = None
+
+ async def __aenter__(self) -> 'InverterG3P':
+ '''Establish a client connection to the TSUN cloud'''
+ connection = asyncio.open_connection(self.host, self.port)
+ reader, writer = await connection
+ self.stream = InverterG3P(reader, writer, self.addr,
+ client_mode=True)
+ logging.info(f'[{self.stream.node_id}:{self.stream.conn_no}] '
+ f'Connected to {self.addr}')
+ self.stream.inc_counter('Inverter_Cnt')
+ await self.stream.publish_outstanding_mqtt()
+ return self.stream
+
+ async def __aexit__(self, exc_type, exc, tb):
+ self.stream.dec_counter('Inverter_Cnt')
+ await self.stream.publish_outstanding_mqtt()
+
+
+class ModbusTcp():
+
+ def __init__(self, loop) -> None:
+ inverters = Config.get('inverters')
+ # logging.info(f'Inverters: {inverters}')
+
+ for inv in inverters.values():
+ if (type(inv) is dict
+ and 'monitor_sn' in inv
+ and 'client_mode' in inv):
+ client = inv['client_mode']
+ # logging.info(f"SerialNo:{inv['monitor_sn']} host:{client['host']} port:{client['port']}") # noqa: E501
+ loop.create_task(self.modbus_loop(client['host'],
+ client['port'],
+ inv['monitor_sn']))
+
+ async def modbus_loop(self, host, port, snr: int) -> None:
+ '''Loop for receiving messages from the TSUN cloud (client-side)'''
+ while True:
+ try:
+ async with ModbusConn(host, port) as stream:
+ await stream.send_start_cmd(snr, host)
+ await stream.loop()
+ logger.info(f'[{stream.node_id}:{stream.conn_no}] '
+ f'Connection closed - Shutdown: '
+ f'{stream.shutdown_started}')
+ if stream.shutdown_started:
+ return
+
+ except (ConnectionRefusedError, TimeoutError) as error:
+ logging.debug(f'Inv-conn:{error}')
+
+ except OSError as error:
+ logging.info(f'os-error: {error}')
+
+ except Exception:
+ logging.error(
+ f"ModbusTcpCreate: Exception for {(host,port)}:\n"
+ f"{traceback.format_exc()}")
+
+ await asyncio.sleep(10)
diff --git a/app/src/mqtt.py b/app/src/mqtt.py
index 2f55660..1ebbd10 100644
--- a/app/src/mqtt.py
+++ b/app/src/mqtt.py
@@ -38,7 +38,8 @@ class Mqtt(metaclass=Singleton):
self.task.cancel()
try:
await self.task
- except Exception as e:
+
+ except (asyncio.CancelledError, Exception) as e:
logging.debug(f"Mqtt.close: exception: {e} ...")
async def publish(self, topic: str, payload: str | bytes | bytearray
@@ -60,6 +61,7 @@ class Mqtt(metaclass=Singleton):
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
@@ -75,6 +77,7 @@ class Mqtt(metaclass=Singleton):
# async with self.__client.messages() as messages:
await self.__client.subscribe(ha_status_topic)
await self.__client.subscribe(mb_rated_topic)
+ await self.__client.subscribe(mb_out_coeff_topic)
await self.__client.subscribe(mb_reads_topic)
await self.__client.subscribe(mb_inputs_topic)
await self.__client.subscribe(mb_at_cmd_topic)
@@ -93,6 +96,19 @@ class Mqtt(metaclass=Singleton):
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)
@@ -154,7 +170,7 @@ class Mqtt(metaclass=Singleton):
logger_mqtt.debug(f'Found: {node_id}')
fnc = getattr(m, "send_modbus_cmd", None)
res = payload.split(',')
- if params != len(res):
+ if params > 0 and params != len(res):
logger_mqtt.error(f'Parameter expected: {params}, '
f'got: {len(res)}')
return
diff --git a/app/src/server.py b/app/src/server.py
index d835e4b..95cc715 100644
--- a/app/src/server.py
+++ b/app/src/server.py
@@ -11,6 +11,7 @@ from gen3.inverter_g3 import InverterG3
from gen3plus.inverter_g3p import InverterG3P
from scheduler import Schedule
from config import Config
+from modbus_tcp import ModbusTcp
routes = web.RouteTableDef()
proxy_is_up = False
@@ -94,6 +95,7 @@ async def handle_shutdown(web_task):
# first, disc all open TCP connections gracefully
#
for stream in Message:
+ stream.shutdown_started = True
try:
await asyncio.wait_for(stream.disc(), 2)
except Exception:
@@ -115,6 +117,13 @@ async def handle_shutdown(web_task):
web_task.cancel()
await web_task
+ #
+ # now cancel all remaining (pending) tasks
+ #
+ pending = asyncio.all_tasks()
+ for task in pending:
+ task.cancel()
+
#
# at last, start a coro for stopping the loop
#
@@ -164,6 +173,7 @@ if __name__ == "__main__":
logging.info(f'ConfigErr: {ConfigErr}')
Inverter.class_init()
Schedule.start()
+ mb_tcp = ModbusTcp(loop)
#
# Create tasks for our listening servers. These must be tasks! If we call
diff --git a/app/tests/test_config.py b/app/tests/test_config.py
index 746d1d8..bff510a 100644
--- a/app/tests/test_config.py
+++ b/app/tests/test_config.py
@@ -20,7 +20,7 @@ def test_empty_config():
Config.conf_schema.validate(cnf)
assert False
except SchemaMissingKeyError:
- assert True
+ pass
def test_default_config():
with open("app/config/default_config.toml", "rb") as f:
@@ -28,10 +28,9 @@ def test_default_config():
try:
validated = Config.conf_schema.validate(cnf)
- assert True
- except:
+ except Exception:
assert False
- assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'monitor_sn': 0, 'suggested_area': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}}
+ 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': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}}
def test_full_config():
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
@@ -41,14 +40,13 @@ def test_full_config():
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
'inverters': {'allow_all': True,
- 'R170000000000001': {'node_id': '', 'suggested_area': '', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}, 'pv3': {'type': 'type3', 'manufacturer': 'man3'}},
- 'Y170000000000001': {'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}}
+ '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': '', 'suggested_area': ''}}}
try:
validated = Config.conf_schema.validate(cnf)
- assert True
- except:
+ except Exception:
assert False
- assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'monitor_sn': 0, 'pv1': {'manufacturer': 'man1','type': 'type1'},'pv2': {'manufacturer': 'man2','type': 'type2'},'pv3': {'manufacturer': 'man3','type': 'type3'}, 'suggested_area': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}}
+ 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():
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
@@ -63,10 +61,9 @@ def test_mininum_config():
try:
validated = Config.conf_schema.validate(cnf)
- assert True
- except:
+ except Exception:
assert False
- assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'monitor_sn': 0, 'suggested_area': ''}}}
+ 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():
cnf = {}
@@ -74,7 +71,7 @@ def test_read_empty():
err = TstConfig.read('app/config/')
assert err == None
cnf = TstConfig.get()
- assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
+ 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': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
defcnf = TstConfig.def_config.get('solarman')
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
@@ -96,7 +93,7 @@ def test_read_cnf1():
err = TstConfig.read('app/config/')
assert err == None
cnf = TstConfig.get()
- assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
+ 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': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
cnf = TstConfig.get('solarman')
assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}
defcnf = TstConfig.def_config.get('solarman')
@@ -109,7 +106,7 @@ def test_read_cnf2():
err = TstConfig.read('app/config/')
assert err == None
cnf = TstConfig.get()
- assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
+ 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': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
assert True == TstConfig.is_default('solarman')
def test_read_cnf3():
@@ -126,7 +123,7 @@ def test_read_cnf4():
err = TstConfig.read('app/config/')
assert err == None
cnf = TstConfig.get()
- assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 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': '', 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
+ 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': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}}
assert False == TstConfig.is_default('solarman')
def test_read_cnf5():
diff --git a/app/tests/test_infos.py b/app/tests/test_infos.py
index 4b23bfb..8d0c268 100644
--- a/app/tests/test_infos.py
+++ b/app/tests/test_infos.py
@@ -1,6 +1,6 @@
# test_with_pytest.py
import pytest
-import json
+import json, math
import logging
from app.src.infos import Register, ClrAtMidnight
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'):
- pass
+ pass # sideeffect is calling generator i.ha_proxy_confs()
val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter
assert val == 0
@@ -123,6 +123,30 @@ def test_table_definition():
val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter
assert val == 3
+def test_table_remove():
+ i = Infos()
+ i.static_init() # initialize counter
+
+ val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter
+ assert val == 0
+
+ # for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'):
+ # pass
+ test = 0
+ for reg in Register:
+ res = i.ha_remove(reg, node_id="garagendach/", snr='123') # noqa: E501
+ if reg == Register.INVERTER_STATUS:
+ test += 1
+ assert res == ('{}', 'sensor', 'garagendach/', 'inv_status_123')
+ elif reg == Register.COLLECT_INTERVAL:
+ test += 1
+ assert res == ('{}', 'sensor', 'garagendach/', 'data_collect_intval_123')
+
+ assert test == 2
+ val = i.dev_value(Register.INTERNAL_ERROR) # check internal error counter
+ assert val == 0
+
+
def test_clr_at_midnight():
i = Infos()
i.static_init() # initialize counter
@@ -198,24 +222,24 @@ def test_get_value():
i.set_db_def_value(Register.PV2_VOLTAGE, 30.3)
assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None)
- assert 30.3 == i.get_db_value(Register.PV2_VOLTAGE, None)
+ assert math.isclose(30.3,i.get_db_value(Register.PV2_VOLTAGE, None), rel_tol=1e-09, abs_tol=1e-09)
def test_update_value():
i = Infos()
assert None == i.get_db_value(Register.PV1_VOLTAGE, None)
keys = i.info_defs[Register.PV1_VOLTAGE]['name']
- name, update = i.update_db(keys, True, 30)
+ _, update = i.update_db(keys, True, 30)
assert update == True
assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None)
keys = i.info_defs[Register.PV1_VOLTAGE]['name']
- name, update = i.update_db(keys, True, 30)
+ _, update = i.update_db(keys, True, 30)
assert update == False
assert 30 == i.get_db_value(Register.PV1_VOLTAGE, None)
keys = i.info_defs[Register.PV1_VOLTAGE]['name']
- name, update = i.update_db(keys, False, 29)
+ _, update = i.update_db(keys, False, 29)
assert update == True
assert 29 == i.get_db_value(Register.PV1_VOLTAGE, None)
diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py
index a127d13..549c8d3 100644
--- a/app/tests/test_infos_g3p.py
+++ b/app/tests/test_infos_g3p.py
@@ -1,12 +1,12 @@
# test_with_pytest.py
-import pytest, json
+import pytest, json, math
from app.src.infos import Register
from app.src.gen3plus.infos_g3p import InfosG3P
from app.src.gen3plus.infos_g3p import RegisterMap
@pytest.fixture
-def DeviceData(): # 0x4110 ftype: 0x02
+def device_data(): # 0x4110 ftype: 0x02
msg = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xba\xd2\x00\x00'
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
@@ -24,7 +24,7 @@ def DeviceData(): # 0x4110 ftype: 0x02
return msg
@pytest.fixture
-def InverterData(): # 0x4210 ftype: 0x01
+def inverter_data(): # 0x4210 ftype: 0x01
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'\x59\x31\x37\x45\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x45'
@@ -56,34 +56,34 @@ def InverterData(): # 0x4210 ftype: 0x01
def test_default_db():
- i = InfosG3P()
+ i = InfosG3P(client_mode=False)
assert json.dumps(i.db) == json.dumps({
- "inverter": {"Manufacturer": "TSUN", "Equipment_Model": "TSOL-MSxx00"},
+ "inverter": {"Manufacturer": "TSUN", "Equipment_Model": "TSOL-MSxx00", "No_Inputs": 4},
"collector": {"Chip_Type": "IGEN TECH"},
})
-def test_parse_4110(DeviceData: bytes):
- i = InfosG3P()
+def test_parse_4110(device_data: bytes):
+ i = InfosG3P(client_mode=False)
i.db.clear()
- for key, update in i.parse (DeviceData, 0x41, 2):
- pass
+ for key, update in i.parse (device_data, 0x41, 2):
+ pass # side effect is calling generator i.parse()
assert json.dumps(i.db) == json.dumps({
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": "192.168.80.49"},
'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "Collector_Fw_Version": "V1.1.00.0B"},
})
-def test_parse_4210(InverterData: bytes):
- i = InfosG3P()
+def test_parse_4210(inverter_data: bytes):
+ i = InfosG3P(client_mode=False)
i.db.clear()
- for key, update in i.parse (InverterData, 0x42, 1):
- pass
+ for key, update in i.parse (inverter_data, 0x42, 1):
+ pass # side effect is calling generator i.parse()
assert json.dumps(i.db) == json.dumps({
"controller": {"Power_On_Time": 2051},
- "inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000, "No_Inputs": 4},
+ "inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000},
"env": {"Inverter_Status": 1, "Inverter_Temp": 14},
"grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8},
"input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76},
@@ -94,7 +94,7 @@ def test_parse_4210(InverterData: bytes):
})
def test_build_ha_conf1():
- i = InfosG3P()
+ i = InfosG3P(client_mode=False)
i.static_init() # initialize counter
tests = 0
@@ -116,8 +116,19 @@ def test_build_ha_conf1():
tests +=1
elif id == 'power_pv2_123':
- assert False # if we haven't received and parsed a control data msg, we don't know the number of inputs. In this case we only register the first one!!
+ assert comp == 'sensor'
+ assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv2_123", "val_tpl": "{{ (value_json['pv2']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV2", "sa": "Module PV2", "via_device": "inverter_123", "ids": ["input_pv2_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
+ tests +=1
+ elif id == 'power_pv3_123':
+ assert comp == 'sensor'
+ assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv3_123", "val_tpl": "{{ (value_json['pv3']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV3", "sa": "Module PV3", "via_device": "inverter_123", "ids": ["input_pv3_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
+ tests +=1
+
+ elif id == 'power_pv4_123':
+ assert comp == 'sensor'
+ assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv4_123", "val_tpl": "{{ (value_json['pv4']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV4", "sa": "Module PV4", "via_device": "inverter_123", "ids": ["input_pv4_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
+ tests +=1
elif id == 'signal_123':
assert comp == 'sensor'
@@ -126,7 +137,7 @@ def test_build_ha_conf1():
elif id == 'inv_count_456':
assert False
- assert tests==4
+ assert tests==7
for d_json, comp, node_id, id in i.ha_proxy_confs(ha_prfx="tsun/", node_id = 'proxy/', snr = '456'):
@@ -138,8 +149,11 @@ def test_build_ha_conf1():
elif id == 'power_pv1_123':
assert False
elif id == 'power_pv2_123':
- assert False # if we haven't received and parsed a control data msg, we don't know the number of inputs. In this case we only register the first one!!
-
+ assert False
+ elif id == 'power_pv3_123':
+ assert False
+ elif id == 'power_pv4_123':
+ assert False
elif id == 'signal_123':
assert False
elif id == 'inv_count_456':
@@ -147,30 +161,99 @@ def test_build_ha_conf1():
assert d_json == json.dumps({"name": "Active Inverter Connections", "stat_t": "tsun/proxy/proxy", "dev_cla": None, "stat_cla": None, "uniq_id": "inv_count_456", "val_tpl": "{{value_json['Inverter_Cnt'] | int}}", "ic": "mdi:counter", "dev": {"name": "Proxy", "sa": "Proxy", "mdl": "proxy", "mf": "Stefan Allius", "sw": "unknown", "ids": ["proxy"]}, "o": {"name": "proxy", "sw": "unknown"}})
tests +=1
- assert tests==5
+ assert tests==8
-def test_exception_and_eval(InverterData: bytes):
+def test_build_ha_conf2():
+ i = InfosG3P(client_mode=True)
+ i.static_init() # initialize counter
+
+ tests = 0
+ for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123'):
+
+ if id == 'out_power_123':
+ assert comp == 'sensor'
+ assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/grid", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "out_power_123", "val_tpl": "{{value_json['Output_Power'] | float}}", "unit_of_meas": "W", "dev": {"name": "Micro Inverter", "sa": "Micro Inverter", "via_device": "controller_123", "mdl": "TSOL-MSxx00", "mf": "TSUN", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
+ tests +=1
+
+ elif id == 'daily_gen_123':
+ assert comp == 'sensor'
+ assert d_json == json.dumps({"name": "Daily Generation", "stat_t": "tsun/garagendach/total", "dev_cla": "energy", "stat_cla": "total_increasing", "uniq_id": "daily_gen_123", "val_tpl": "{{value_json['Daily_Generation'] | float}}", "unit_of_meas": "kWh", "ic": "mdi:solar-power-variant", "dev": {"name": "Micro Inverter", "sa": "Micro Inverter", "via_device": "controller_123", "mdl": "TSOL-MSxx00", "mf": "TSUN", "ids": ["inverter_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
+ tests +=1
+
+ elif id == 'power_pv1_123':
+ assert comp == 'sensor'
+ assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv1_123", "val_tpl": "{{ (value_json['pv1']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV1", "sa": "Module PV1", "via_device": "inverter_123", "ids": ["input_pv1_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
+ tests +=1
+
+ elif id == 'power_pv2_123':
+ assert comp == 'sensor'
+ assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv2_123", "val_tpl": "{{ (value_json['pv2']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV2", "sa": "Module PV2", "via_device": "inverter_123", "ids": ["input_pv2_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
+ tests +=1
+
+ elif id == 'power_pv3_123':
+ assert comp == 'sensor'
+ assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv3_123", "val_tpl": "{{ (value_json['pv3']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV3", "sa": "Module PV3", "via_device": "inverter_123", "ids": ["input_pv3_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
+ tests +=1
+
+ elif id == 'power_pv4_123':
+ assert comp == 'sensor'
+ assert d_json == json.dumps({"name": "Power", "stat_t": "tsun/garagendach/input", "dev_cla": "power", "stat_cla": "measurement", "uniq_id": "power_pv4_123", "val_tpl": "{{ (value_json['pv4']['Power'] | float)}}", "unit_of_meas": "W", "dev": {"name": "Module PV4", "sa": "Module PV4", "via_device": "inverter_123", "ids": ["input_pv4_123"]}, "o": {"name": "proxy", "sw": "unknown"}})
+ tests +=1
+
+ elif id == 'signal_123':
+ assert comp == 'sensor'
+ assert d_json == json.dumps({})
+ tests +=1
+ elif id == 'inv_count_456':
+ assert False
+
+ assert tests==7
+
+
+ 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':
+ assert False
+ elif id == 'daily_gen_123':
+ assert False
+ elif id == 'power_pv1_123':
+ assert False
+ elif id == 'power_pv2_123':
+ assert False
+ elif id == 'power_pv3_123':
+ assert False
+ elif id == 'power_pv4_123':
+ assert False
+ elif id == 'signal_123':
+ assert False
+ elif id == 'inv_count_456':
+ assert comp == 'sensor'
+ assert d_json == json.dumps({"name": "Active Inverter Connections", "stat_t": "tsun/proxy/proxy", "dev_cla": None, "stat_cla": None, "uniq_id": "inv_count_456", "val_tpl": "{{value_json['Inverter_Cnt'] | int}}", "ic": "mdi:counter", "dev": {"name": "Proxy", "sa": "Proxy", "mdl": "proxy", "mf": "Stefan Allius", "sw": "unknown", "ids": ["proxy"]}, "o": {"name": "proxy", "sw": "unknown"}})
+ tests +=1
+
+ assert tests==8
+
+def test_exception_and_eval(inverter_data: bytes):
# add eval to convert temperature from °F to °C
RegisterMap.map[0x420100d8]['eval'] = '(result-32)/1.8'
# map PV1_VOLTAGE to invalid register
RegisterMap.map[0x420100e0]['reg'] = Register.TEST_REG2
# 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'
- i = InfosG3P()
+ i = InfosG3P(client_mode=False)
# i.db.clear()
- for key, update in i.parse (InverterData, 0x42, 1):
- pass
- assert 12.2222 == round (i.get_db_value(Register.INVERTER_TEMP, 0),4)
-
+ for key, update in i.parse (inverter_data, 0x42, 1):
+ pass # side effect is calling generator i.parse()
+ assert math.isclose(12.2222, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09)
del RegisterMap.map[0x420100d8]['eval'] # remove eval
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 (InverterData, 0x42, 1):
- pass
+ for key, update in i.parse (inverter_data, 0x42, 1):
+ pass # side effect is calling generator i.parse()
assert 54 == i.get_db_value(Register.INVERTER_TEMP, 0)
\ No newline at end of file
diff --git a/app/tests/test_modbus.py b/app/tests/test_modbus.py
index 0009182..970a161 100644
--- a/app/tests/test_modbus.py
+++ b/app/tests/test_modbus.py
@@ -5,7 +5,6 @@ from app.src.modbus import Modbus
from app.src.infos import Infos, Register
pytest_plugins = ('pytest_asyncio',)
-# pytestmark = pytest.mark.asyncio(scope="module")
class ModbusTestHelper(Modbus):
def __init__(self):
@@ -32,7 +31,12 @@ def test_modbus_crc():
assert mb._Modbus__check_crc(b'\x01\x06\x20\x08\x00\x00\x03\xc8')
assert 0x5c75 == mb._Modbus__calc_crc(b'\x01\x03\x08\x01\x2c\x00\x2c\x02\x2c\x2c\x46')
-
+ msg = b'\x01\x03\x28\x51'
+ msg += b'\x0e\x08\xd3\x00\x29\x13\x87\x00\x3e\x00\x00\x01\x2c\x03\xb4\x00'
+ msg += b'\x08\x00\x00\x00\x00\x01\x59\x01\x21\x03\xe6\x00\x00\x00\x00\x00'
+ msg += b'\x00\x00\x00\x00\x00\x00\x00\xe6\xef'
+ assert 0 == mb._Modbus__calc_crc(msg)
+
def test_build_modbus_pdu():
'''Check building and sending a MODBUS RTU'''
mb = ModbusTestHelper()
@@ -71,8 +75,8 @@ def test_recv_resp_crc_err():
mb.req_pend = True
mb.last_addr = 1
mb.last_fcode = 3
- mb.last_reg == 0x300e
- mb.last_len == 2
+ mb.last_reg = 0x300e
+ mb.last_len = 2
# check matching response, but with CRC error
call = 0
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x04\x01\x2c\x00\x46\xbb\xf3', 'test'):
@@ -91,8 +95,8 @@ def test_recv_resp_invalid_addr():
# simulate a transmitted request
mb.last_addr = 1
mb.last_fcode = 3
- mb.last_reg == 0x300e
- mb.last_len == 2
+ mb.last_reg = 0x300e
+ mb.last_len = 2
# check not matching response, with wrong server addr
call = 0
@@ -173,7 +177,7 @@ def test_parse_resp():
assert mb.req_pend
call = 0
- exp_result = ['V0.0.212', 4.4, 0.7, 0.7, 30]
+ exp_result = ['V0.0.2C', 4.4, 0.7, 0.7, 30]
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
if key == 'grid':
assert update == True
@@ -222,7 +226,7 @@ def test_queue2():
assert mb.send_calls == 1
assert mb.pdu == b'\x01\x030\x07\x00\x06{\t'
call = 0
- exp_result = ['V0.0.212', 4.4, 0.7, 0.7, 30]
+ exp_result = ['V0.0.2C', 4.4, 0.7, 0.7, 30]
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
if key == 'grid':
assert update == True
@@ -242,7 +246,7 @@ def test_queue2():
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'):
- pass
+ pass # call generator mb.recv_resp()
assert mb.que.qsize() == 0
assert mb.send_calls == 3
@@ -272,7 +276,7 @@ def test_queue3():
assert mb.recv_responses == 0
call = 0
- exp_result = ['V0.0.212', 4.4, 0.7, 0.7, 30]
+ exp_result = ['V0.0.2C', 4.4, 0.7, 0.7, 30]
for key, update, val in mb.recv_resp(mb.db, b'\x01\x03\x0c\x01\x2c\x00\x2c\x00\x2c\x00\x46\x00\x46\x00\x46\x32\xc8', 'test'):
if key == 'grid':
assert update == True
@@ -293,7 +297,7 @@ def test_queue3():
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'):
- pass
+ pass # no code in loop is OK; calling the generator is the purpose
assert 0 == mb.err
assert mb.recv_responses == 2
@@ -359,8 +363,6 @@ async def test_timeout():
assert mb.retry_cnt == 0
assert mb.send_calls == 4
- # assert mb.counter == {}
-
def test_recv_unknown_data():
'''Receive a response with an unknwon register'''
mb = ModbusTestHelper()
@@ -378,3 +380,16 @@ def test_recv_unknown_data():
assert not mb.req_pend
del mb.map[0x9000]
+
+def test_close():
+ '''Check queue handling for build_msg() calls'''
+ mb = ModbusTestHelper()
+ mb.build_msg(1,3,0x3007,6)
+ mb.build_msg(1,6,0x2008,4)
+ assert mb.que.qsize() == 1
+ mb.build_msg(1,3,0x3007,6)
+ assert mb.que.qsize() == 2
+ assert mb.que.empty() == False
+ mb.close()
+ assert mb.que.qsize() == 0
+ assert mb.que.empty() == True
diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py
index 54e4878..ac76aae 100644
--- a/app/tests/test_solarman.py
+++ b/app/tests/test_solarman.py
@@ -1,8 +1,8 @@
import pytest
import struct
import time
+import asyncio
import logging
-from datetime import datetime
from app.src.gen3plus.solarman_v5 import SolarmanV5
from app.src.config import Config
from app.src.infos import Infos, Register
@@ -38,9 +38,11 @@ class Mqtt():
class MemoryStream(SolarmanV5):
def __init__(self, msg, chunks = (0,), server_side: bool = True):
- super().__init__(server_side)
+ super().__init__(server_side, client_mode=False)
if server_side:
- self.mb.timeout = 1 # overwrite for faster testing
+ self.mb.timeout = 0.4 # overwrite for faster testing
+ self.mb_start_timeout = 0.5
+ self.mb_timeout = 0.5
self.writer = Writer()
self.mqtt = Mqtt()
self.__msg = msg
@@ -58,6 +60,7 @@ class MemoryStream(SolarmanV5):
self.at_acl = {'mqtt': {'allow': ['AT+'], 'block': ['AT+WEBU']}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE', 'AT+TIME'], 'block': ['AT+WEBU']}}
self.key = ''
self.data = ''
+ self.msg_recvd = []
def _timestamp(self):
return timestamp
@@ -68,6 +71,7 @@ class MemoryStream(SolarmanV5):
def append_msg(self, msg):
self.__msg += msg
self.__msg_len += len(msg)
+ self.__chunk_idx = 0
def publish_mqtt(self, key, data):
self.key = key
@@ -77,18 +81,18 @@ class MemoryStream(SolarmanV5):
copied_bytes = 0
try:
if (self.__offs < self.__msg_len):
- len = self.__chunks[self.__chunk_idx]
+ chunk_len = self.__chunks[self.__chunk_idx]
self.__chunk_idx += 1
- if len!=0:
- self._recv_buffer += self.__msg[self.__offs:len]
- copied_bytes = len - self.__offs
- self.__offs = len
+ if chunk_len!=0:
+ self._recv_buffer += self.__msg[self.__offs:chunk_len]
+ copied_bytes = chunk_len - self.__offs
+ self.__offs = chunk_len
else:
self._recv_buffer += self.__msg[self.__offs:]
copied_bytes = self.__msg_len - self.__offs
self.__offs = self.__msg_len
- except:
- pass
+ except Exception:
+ pass # ignore exceptions here
return copied_bytes
async def async_write(self, headline=''):
@@ -97,14 +101,20 @@ class MemoryStream(SolarmanV5):
def createClientStream(self, msg, chunks = (0,)):
c = MemoryStream(msg, chunks, False)
- self.remoteStream = c
- c. remoteStream = self
+ self.remote_stream = c
+ c. remote_stream = self
return c
def _SolarmanV5__flush_recv_msg(self) -> None:
+ self.msg_recvd.append(
+ {
+ 'control': self.control,
+ 'seq': str(self.seq),
+ 'data_len': self.data_len
+ }
+ )
super()._SolarmanV5__flush_recv_msg()
self.msg_count += 1
- return
def get_sn() -> bytes:
@@ -138,7 +148,7 @@ def incorrect_checksum(buf):
return checksum.to_bytes(length=1)
@pytest.fixture
-def DeviceIndMsg(): # 0x4110
+def device_ind_msg(): # 0x4110
msg = b'\xa5\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
msg += b'\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'
@@ -158,7 +168,7 @@ def DeviceIndMsg(): # 0x4110
return msg
@pytest.fixture
-def DeviceRspMsg(): # 0x1110
+def device_rsp_msg(): # 0x1110
msg = b'\xa5\x0a\x00\x10\x11\x01\x01' +get_sn() +b'\x02\x01'
msg += total()
msg += hb()
@@ -167,7 +177,7 @@ def DeviceRspMsg(): # 0x1110
return msg
@pytest.fixture
-def InvalidStartByte(): # 0x4110
+def invalid_start_byte(): # 0x4110
msg = b'\xa4\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
@@ -187,7 +197,7 @@ def InvalidStartByte(): # 0x4110
return msg
@pytest.fixture
-def InvalidStopByte(): # 0x4110
+def invalid_stop_byte(): # 0x4110
msg = b'\xa5\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
@@ -207,7 +217,7 @@ def InvalidStopByte(): # 0x4110
return msg
@pytest.fixture
-def InvalidChecksum(): # 0x4110
+def invalid_checksum(): # 0x4110
msg = b'\xa5\xd4\x00\x10\x41\x00\x01' +get_sn() +b'\x02\xba\xd2\x00\x00'
msg += b'\x19\x00\x00\x00\x00\x00\x00\x00\x05\x3c\x78\x01\x64\x01\x4c\x53'
msg += b'\x57\x35\x42\x4c\x45\x5f\x31\x37\x5f\x30\x32\x42\x30\x5f\x31\x2e'
@@ -227,7 +237,7 @@ def InvalidChecksum(): # 0x4110
return msg
@pytest.fixture
-def InverterIndMsg(): # 0x4210
+def inverter_ind_msg(): # 0x4210
msg = b'\xa5\x99\x01\x10\x42\x01\x02' +get_sn() +b'\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'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
@@ -260,7 +270,7 @@ def InverterIndMsg(): # 0x4210
return msg
@pytest.fixture
-def InverterIndMsg1600(): # 0x4210 rated Power 1600W
+def inverter_ind_msg1600(): # 0x4210 rated Power 1600W
msg = b'\xa5\x99\x01\x10\x42\xe6\x9e' +get_sn() +b'\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'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
@@ -293,7 +303,7 @@ def InverterIndMsg1600(): # 0x4210 rated Power 1600W
return msg
@pytest.fixture
-def InverterIndMsg1800(): # 0x4210 rated Power 1800W
+def inverter_ind_msg1800(): # 0x4210 rated Power 1800W
msg = b'\xa5\x99\x01\x10\x42\xe6\x9e' +get_sn() +b'\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'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
@@ -326,7 +336,7 @@ def InverterIndMsg1800(): # 0x4210 rated Power 1800W
return msg
@pytest.fixture
-def InverterIndMsg2000(): # 0x4210 rated Power 2000W
+def inverter_ind_msg2000(): # 0x4210 rated Power 2000W
msg = b'\xa5\x99\x01\x10\x42\xe6\x9e' +get_sn() +b'\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'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
@@ -359,7 +369,7 @@ def InverterIndMsg2000(): # 0x4210 rated Power 2000W
return msg
@pytest.fixture
-def InverterIndMsg800(): # 0x4210 rated Power 800W
+def inverter_ind_msg800(): # 0x4210 rated Power 800W
msg = b'\xa5\x99\x01\x10\x42\xe6\x9e' +get_sn() +b'\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'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
@@ -392,7 +402,7 @@ def InverterIndMsg800(): # 0x4210 rated Power 800W
return msg
@pytest.fixture
-def InverterIndMsg_81(): # 0x4210 fcode 0x81
+def inverter_ind_msg_81(): # 0x4210 fcode 0x81
msg = b'\xa5\x99\x01\x10\x42\x02\x03' +get_sn() +b'\x81\xb0\x02\xbc\xc8'
msg += b'\x24\x32\x6c\x1f\x00\x00\xa0\x07\x04\x03\x01\x00\x03\x08\x00\x00'
msg += b'\x59\x31\x37\x45\x37\x41\x30\x46\x30\x31\x30\x42\x30\x31\x33\x45'
@@ -425,7 +435,7 @@ def InverterIndMsg_81(): # 0x4210 fcode 0x81
return msg
@pytest.fixture
-def InverterRspMsg(): # 0x1210
+def inverter_rsp_msg(): # 0x1210
msg = b'\xa5\x0a\x00\x10\x12\x02\02' +get_sn() +b'\x01\x01'
msg += total()
msg += hb()
@@ -434,7 +444,7 @@ def InverterRspMsg(): # 0x1210
return msg
@pytest.fixture
-def InverterRspMsg_81(): # 0x1210 fcode 0x81
+def inverter_rsp_msg_81(): # 0x1210 fcode 0x81
msg = b'\xa5\x0a\x00\x10\x12\x03\03' +get_sn() +b'\x81\x01'
msg += total()
msg += hb()
@@ -443,7 +453,7 @@ def InverterRspMsg_81(): # 0x1210 fcode 0x81
return msg
@pytest.fixture
-def UnknownMsg(): # 0x5110
+def unknown_msg(): # 0x5110
msg = b'\xa5\x0a\x00\x10\x51\x10\x84' +get_sn() +b'\x01\x01\x69\x6f\x09'
msg += b'\x66\x78\x00\x00\x00'
msg += correct_checksum(msg)
@@ -451,7 +461,7 @@ def UnknownMsg(): # 0x5110
return msg
@pytest.fixture
-def SyncStartIndMsg(): # 0x4310
+def sync_start_ind_msg(): # 0x4310
msg = b'\xa5\x2f\x00\x10\x43\x0c\x0d' +get_sn() +b'\x81\x7a\x0b\x2e\x32'
msg += b'\x39\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x41\x6c\x6c\x69\x75\x73'
msg += b'\x2d\x48\x6f\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
@@ -461,7 +471,7 @@ def SyncStartIndMsg(): # 0x4310
return msg
@pytest.fixture
-def SyncStartRspMsg(): # 0x1310
+def sync_start_rsp_msg(): # 0x1310
msg = b'\xa5\x0a\x00\x10\x13\x0d\x0d' +get_sn() +b'\x81\x01'
msg += total()
msg += hb()
@@ -470,7 +480,7 @@ def SyncStartRspMsg(): # 0x1310
return msg
@pytest.fixture
-def SyncStartFwdMsg(): # 0x4310
+def sync_start_fwd_msg(): # 0x4310
msg = b'\xa5\x2f\x00\x10\x43\x0d\x0e' +get_sn() +b'\x81\x7a\x0b\x2e\x32'
msg += b'\x39\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x41\x6c\x6c\x69\x75\x73'
msg += b'\x2d\x48\x6f\x6d\x65\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
@@ -481,7 +491,7 @@ def SyncStartFwdMsg(): # 0x4310
@pytest.fixture
-def AtCommandIndMsg(): # 0x4510
+def at_command_ind_msg(): # 0x4510
msg = b'\xa5\x27\x00\x10\x45\x03\x02' +get_sn() +b'\x01\x02\x00'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
msg += b'AT+TIME=214028,1,60,120\r'
@@ -490,7 +500,7 @@ def AtCommandIndMsg(): # 0x4510
return msg
@pytest.fixture
-def AtCommandIndMsgBlock(): # 0x4510
+def at_command_ind_msg_block(): # 0x4510
msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x01\x02\x00'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
msg += b'AT+WEBU\r'
@@ -499,7 +509,7 @@ def AtCommandIndMsgBlock(): # 0x4510
return msg
@pytest.fixture
-def AtCommandRspMsg(): # 0x1510
+def at_command_rsp_msg(): # 0x1510
msg = b'\xa5\x11\x00\x10\x15\x03\x03' +get_sn() +b'\x01\x01'
msg += total()
msg += hb()
@@ -509,7 +519,7 @@ def AtCommandRspMsg(): # 0x1510
return msg
@pytest.fixture
-def HeartbeatIndMsg(): # 0x4710
+def heartbeat_ind_msg(): # 0x4710
msg = b'\xa5\x01\x00\x10\x47\x10\x84' +get_sn()
msg += b'\x00'
msg += correct_checksum(msg)
@@ -517,7 +527,7 @@ def HeartbeatIndMsg(): # 0x4710
return msg
@pytest.fixture
-def HeartbeatRspMsg(): # 0x1710
+def heartbeat_rsp_msg(): # 0x1710
msg = b'\xa5\x0a\x00\x10\x17\x11\x84' +get_sn() +b'\x00\x01'
msg += total()
msg += hb()
@@ -526,7 +536,7 @@ def HeartbeatRspMsg(): # 0x1710
return msg
@pytest.fixture
-def SyncEndIndMsg(): # 0x4810
+def sync_end_ind_msg(): # 0x4810
msg = b'\xa5\x3c\x00\x10\x48\x06\x07' +get_sn() +b'\x01\xa5\x3c\x2e\x32'
msg += b'\x2c\x00\x00\x00\xc1\x01\xec\x33\x01\x05\x2c\xff\xff\xff\xff\xff'
msg += b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'
@@ -537,7 +547,7 @@ def SyncEndIndMsg(): # 0x4810
return msg
@pytest.fixture
-def SyncEndRspMsg(): # 0x1810
+def sync_end_rsp_msg(): # 0x1810
msg = b'\xa5\x0a\x00\x10\x18\x07\x07' +get_sn() +b'\x01\x01'
msg += total()
msg += hb()
@@ -546,7 +556,7 @@ def SyncEndRspMsg(): # 0x1810
return msg
@pytest.fixture
-def MsgModbusCmd():
+def msg_modbus_cmd():
msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x02\xb0\x02'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x06\x20\x08'
msg += b'\x00\x00\x03\xc8'
@@ -555,7 +565,7 @@ def MsgModbusCmd():
return msg
@pytest.fixture
-def MsgModbusCmdFwd():
+def msg_modbus_cmd_fwd():
msg = b'\xa5\x17\x00\x10\x45\x01\x00' +get_sn() +b'\x02\xb0\x02'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x06\x20\x08'
msg += b'\x00\x00\x03\xc8'
@@ -564,7 +574,7 @@ def MsgModbusCmdFwd():
return msg
@pytest.fixture
-def MsgModbusCmdCrcErr():
+def msg_modbus_cmd_crc_err():
msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x02\xb0\x02'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x06\x20\x08'
msg += b'\x00\x00\x04\xc8'
@@ -573,7 +583,7 @@ def MsgModbusCmdCrcErr():
return msg
@pytest.fixture
-def MsgModbusRsp(): # 0x1510
+def msg_modbus_rsp(): # 0x1510
msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x01'
msg += total()
msg += hb()
@@ -586,7 +596,7 @@ def MsgModbusRsp(): # 0x1510
return msg
@pytest.fixture
-def MsgModbusInvalid(): # 0x1510
+def msg_modbus_invalid(): # 0x1510
msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x02\x00'
msg += total()
msg += hb()
@@ -599,7 +609,7 @@ def MsgModbusInvalid(): # 0x1510
return msg
@pytest.fixture
-def MsgUnknownCmd():
+def msg_unknown_cmd():
msg = b'\xa5\x17\x00\x10\x45\x03\x02' +get_sn() +b'\x03\xb0\x02'
msg += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x06\x20\x08'
msg += b'\x00\x00\x03\xc8'
@@ -608,7 +618,7 @@ def MsgUnknownCmd():
return msg
@pytest.fixture
-def MsgUnknownCmdRsp(): # 0x1510
+def msg_unknown_cmd_rsp(): # 0x1510
msg = b'\xa5\x3b\x00\x10\x15\x03\x03' +get_sn() +b'\x03\x01'
msg += total()
msg += hb()
@@ -621,19 +631,19 @@ def MsgUnknownCmdRsp(): # 0x1510
return msg
@pytest.fixture
-def ConfigTsunAllowAll():
+def config_tsun_allow_all():
Config.config = {'solarman':{'enabled': True}, 'inverters':{'allow_all':True}}
@pytest.fixture
-def ConfigNoTsunInv1():
- Config.config = {'solarman':{'enabled': False},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889,'node_id':'inv1','suggested_area':'roof'}}}
+def config_no_tsun_inv1():
+ Config.config = {'solarman':{'enabled': False},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}}
@pytest.fixture
-def ConfigTsunInv1():
- Config.config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889,'node_id':'inv1','suggested_area':'roof'}}}
+def config_tsun_inv1():
+ Config.config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}}
-def test_read_message(DeviceIndMsg):
- m = MemoryStream(DeviceIndMsg, (0,))
+def test_read_message(device_ind_msg):
+ m = MemoryStream(device_ind_msg, (0,))
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
@@ -649,12 +659,12 @@ def test_read_message(DeviceIndMsg):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_invalid_start_byte(InvalidStartByte, DeviceIndMsg):
+def test_invalid_start_byte(invalid_start_byte, device_ind_msg):
# received a message with wrong start byte plus an valid message
# the complete receive buffer must be cleared to
# find the next valid message
- m = MemoryStream(InvalidStartByte, (0,))
- m.append_msg(DeviceIndMsg)
+ m = MemoryStream(invalid_start_byte, (0,))
+ m.append_msg(device_ind_msg)
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since start byte is wrong
assert m.msg_count == 0
@@ -670,11 +680,11 @@ def test_invalid_start_byte(InvalidStartByte, DeviceIndMsg):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
m.close()
-def test_invalid_stop_byte(InvalidStopByte):
+def test_invalid_stop_byte(invalid_stop_byte):
# received a message with wrong stop byte
# the complete receive buffer must be cleared to
# find the next valid message
- m = MemoryStream(InvalidStopByte, (0,))
+ m = MemoryStream(invalid_stop_byte, (0,))
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since start byte is wrong
assert m.msg_count == 1 # msg flush was called
@@ -690,47 +700,38 @@ def test_invalid_stop_byte(InvalidStopByte):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
m.close()
-def test_invalid_stop_byte2(InvalidStopByte, DeviceIndMsg):
+def test_invalid_stop_byte2(invalid_stop_byte, device_ind_msg):
# received a message with wrong stop byte plus an valid message
# only the first message must be discarded
- m = MemoryStream(InvalidStopByte, (0,))
- m.append_msg(DeviceIndMsg)
- m.read() # read complete msg, and dispatch msg
- assert not m.header_valid # must be invalid, since start byte is wrong
- assert m.msg_count == 1 # msg flush was called
- assert m.header_len==11
- assert m.snr == 2070233889
- assert m.unique_id == 0
- assert m.control == 0x4110
- assert str(m.seq) == '01:00'
- assert m.data_len == 0xd4
- assert m._recv_buffer==DeviceIndMsg
- assert m._send_buffer==b''
- assert m._forward_buffer==b''
- assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
+ m = MemoryStream(invalid_stop_byte, (0,))
+ m.append_msg(device_ind_msg)
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 2
assert m.header_len==11
assert m.snr == 2070233889
+ assert m.msg_recvd[0]['control']==0x4110
+ assert m.msg_recvd[0]['seq']=='01:00'
+ assert m.msg_recvd[0]['data_len']==0xd4
+ assert m.msg_recvd[1]['control']==0x4110
+ assert m.msg_recvd[1]['seq']=='01:00'
+ assert m.msg_recvd[1]['data_len']==0xd4
+
assert m.unique_id == None
- assert m.control == 0x4110
- assert str(m.seq) == '01:00'
- assert m.data_len == 0xd4
assert m._recv_buffer==b''
assert m._send_buffer==b''
assert m._forward_buffer==b''
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
m.close()
-def test_invalid_stop_start_byte(InvalidStopByte, InvalidStartByte):
+def test_invalid_stop_start_byte(invalid_stop_byte, invalid_start_byte):
# received a message with wrong stop byte plus an invalid message
# with fron start byte
# the complete receive buffer must be cleared to
# find the next valid message
- m = MemoryStream(InvalidStopByte, (0,))
- m.append_msg(InvalidStartByte)
+ m = MemoryStream(invalid_stop_byte, (0,))
+ m.append_msg(invalid_start_byte)
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since start byte is wrong
assert m.msg_count == 1 # msg flush was called
@@ -746,24 +747,11 @@ def test_invalid_stop_start_byte(InvalidStopByte, InvalidStartByte):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
m.close()
-def test_invalid_checksum(InvalidChecksum, DeviceIndMsg):
+def test_invalid_checksum(invalid_checksum, device_ind_msg):
# received a message with wrong checksum plus an valid message
# only the first message must be discarded
- m = MemoryStream(InvalidChecksum, (0,))
- m.append_msg(DeviceIndMsg)
- m.read() # read complete msg, and dispatch msg
- assert not m.header_valid # must be invalid, since start byte is wrong
- assert m.msg_count == 1 # msg flush was called
- assert m.header_len==11
- assert m.snr == 2070233889
- assert m.unique_id == 0
- assert m.control == 0x4110
- assert str(m.seq) == '01:00'
- assert m.data_len == 0xd4
- assert m._recv_buffer==DeviceIndMsg
- assert m._send_buffer==b''
- assert m._forward_buffer==b''
- assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
+ m = MemoryStream(invalid_checksum, (0,))
+ m.append_msg(device_ind_msg)
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -771,49 +759,41 @@ def test_invalid_checksum(InvalidChecksum, DeviceIndMsg):
assert m.header_len==11
assert m.snr == 2070233889
assert m.unique_id == None
- assert m.control == 0x4110
- assert str(m.seq) == '01:00'
- assert m.data_len == 0xd4
+ assert m.msg_recvd[0]['control']==0x4110
+ assert m.msg_recvd[0]['seq']=='01:00'
+ assert m.msg_recvd[0]['data_len']==0xd4
+ assert m.msg_recvd[1]['control']==0x4110
+ assert m.msg_recvd[1]['seq']=='01:00'
+ assert m.msg_recvd[1]['data_len']==0xd4
assert m._recv_buffer==b''
assert m._send_buffer==b''
assert m._forward_buffer==b''
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
m.close()
-def test_read_message_twice(ConfigNoTsunInv1, DeviceIndMsg, DeviceRspMsg):
- ConfigNoTsunInv1
- m = MemoryStream(DeviceIndMsg, (0,))
- m.append_msg(DeviceIndMsg)
- m.read() # read complete msg, and dispatch msg
- assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
- assert m.msg_count == 1
- assert m.header_len==11
- assert m.snr == 2070233889
- assert m.unique_id == '2070233889'
- assert m.control == 0x4110
- assert str(m.seq) == '01:01'
- assert m.data_len == 0xd4
- assert m._send_buffer==DeviceRspMsg
- assert m._forward_buffer==b''
- assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
-
- m._send_buffer = bytearray(0) # clear send buffer for next test
+def test_read_message_twice(config_no_tsun_inv1, device_ind_msg, device_rsp_msg):
+ config_no_tsun_inv1
+ m = MemoryStream(device_ind_msg, (0,))
+ m.append_msg(device_ind_msg)
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 2
assert m.header_len==11
assert m.snr == 2070233889
assert m.unique_id == '2070233889'
- assert m.control == 0x4110
- assert str(m.seq) == '01:01'
- assert m.data_len == 0xd4
- assert m._send_buffer==DeviceRspMsg
+ assert m.msg_recvd[0]['control']==0x4110
+ assert m.msg_recvd[0]['seq']=='01:01'
+ assert m.msg_recvd[0]['data_len']==0xd4
+ assert m.msg_recvd[1]['control']==0x4110
+ assert m.msg_recvd[1]['seq']=='01:01'
+ assert m.msg_recvd[1]['data_len']==0xd4
+ assert m._send_buffer==device_rsp_msg+device_rsp_msg
assert m._forward_buffer==b''
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_read_message_in_chunks(DeviceIndMsg):
- m = MemoryStream(DeviceIndMsg, (4,11,0))
+def test_read_message_in_chunks(device_ind_msg):
+ m = MemoryStream(device_ind_msg, (4,11,0))
m.read() # read 4 bytes, header incomplere
assert not m.header_valid # must be invalid, since header not complete
assert m.msg_count == 0
@@ -833,9 +813,9 @@ def test_read_message_in_chunks(DeviceIndMsg):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_read_message_in_chunks2(ConfigTsunInv1, DeviceIndMsg):
- ConfigTsunInv1
- m = MemoryStream(DeviceIndMsg, (4,10,0))
+def test_read_message_in_chunks2(config_tsun_inv1, device_ind_msg):
+ config_tsun_inv1
+ m = MemoryStream(device_ind_msg, (4,10,0))
m.read() # read 4 bytes, header incomplere
assert not m.header_valid
assert m.msg_count == 0
@@ -858,31 +838,12 @@ def test_read_message_in_chunks2(ConfigTsunInv1, DeviceIndMsg):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_read_two_messages(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, InverterIndMsg, InverterRspMsg):
- ConfigTsunAllowAll
- m = MemoryStream(DeviceIndMsg, (0,))
- m.append_msg(InverterIndMsg)
- m.read() # read complete msg, and dispatch msg
- assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
- assert m.msg_count == 1
- assert m.header_len==11
- assert m.snr == 2070233889
- assert m.unique_id == '2070233889'
- assert m.control == 0x4110
- assert str(m.seq) == '01:01'
- assert m.data_len == 0xd4
- assert m.msg_count == 1
- assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
- assert m._forward_buffer==DeviceIndMsg
- assert m._send_buffer==DeviceRspMsg
-
- m._send_buffer = bytearray(0) # clear send buffer for next test
+def test_read_two_messages(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inverter_ind_msg, inverter_rsp_msg):
+ config_tsun_allow_all
+ m = MemoryStream(device_ind_msg, (0,))
+ m.append_msg(inverter_ind_msg)
+
m._init_new_client_conn()
- assert m._send_buffer==b''
- assert m._recv_buffer==InverterIndMsg
-
- m._send_buffer = bytearray(0) # clear send buffer for next test
- m._forward_buffer = bytearray(0) # clear forward buffer for next test
m.read() # read complete msg, and dispatch msg
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -890,44 +851,24 @@ def test_read_two_messages(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, Inver
assert m.header_len==11
assert m.snr == 2070233889
assert m.unique_id == '2070233889'
- assert m.control == 0x4210
- assert str(m.seq) == '02:02'
- assert m.data_len == 0x199
- assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
- assert m._forward_buffer==InverterIndMsg
- assert m._send_buffer==InverterRspMsg
+ assert m.msg_recvd[0]['control']==0x4110
+ assert m.msg_recvd[0]['seq']=='01:01'
+ assert m.msg_recvd[0]['data_len']==0xd4
+ assert m.msg_recvd[1]['control']==0x4210
+ assert m.msg_recvd[1]['seq']=='02:02'
+ assert m.msg_recvd[1]['data_len']==0x199
+ assert m._forward_buffer==device_ind_msg+inverter_ind_msg
+ assert m._send_buffer==device_rsp_msg+inverter_rsp_msg
m._send_buffer = bytearray(0) # clear send buffer for next test
m._init_new_client_conn()
assert m._send_buffer==b''
m.close()
-def test_read_two_messages2(ConfigTsunAllowAll, InverterIndMsg, InverterIndMsg_81, InverterRspMsg, InverterRspMsg_81):
- ConfigTsunAllowAll
- m = MemoryStream(InverterIndMsg, (0,))
- m.append_msg(InverterIndMsg_81)
- m.read() # read complete msg, and dispatch msg
- assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
- assert m.msg_count == 1
- assert m.header_len==11
- assert m.snr == 2070233889
- assert m.unique_id == '2070233889'
- assert m.control == 0x4210
- assert m.time_ofs == 0x33e447a0
- assert str(m.seq) == '02:02'
- assert m.data_len == 0x199
- assert m.msg_count == 1
- assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
- assert m._forward_buffer==InverterIndMsg
- assert m._send_buffer==InverterRspMsg
-
- m._send_buffer = bytearray(0) # clear send buffer for next test
- m._init_new_client_conn()
- assert m._send_buffer==b''
- assert m._recv_buffer==InverterIndMsg_81
-
- m._send_buffer = bytearray(0) # clear send buffer for next test
- m._forward_buffer = bytearray(0) # clear forward buffer for next test
+def test_read_two_messages2(config_tsun_allow_all, inverter_ind_msg, inverter_ind_msg_81, inverter_rsp_msg, inverter_rsp_msg_81):
+ config_tsun_allow_all
+ m = MemoryStream(inverter_ind_msg, (0,))
+ m.append_msg(inverter_ind_msg_81)
m.read() # read complete msg, and dispatch msg
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -935,22 +876,24 @@ def test_read_two_messages2(ConfigTsunAllowAll, InverterIndMsg, InverterIndMsg_8
assert m.header_len==11
assert m.snr == 2070233889
assert m.unique_id == '2070233889'
- assert m.control == 0x4210
+ assert m.msg_recvd[0]['control']==0x4210
+ assert m.msg_recvd[0]['seq']=='02:02'
+ assert m.msg_recvd[0]['data_len']==0x199
+ assert m.msg_recvd[1]['control']==0x4210
+ assert m.msg_recvd[1]['seq']=='03:03'
+ assert m.msg_recvd[1]['data_len']==0x199
assert m.time_ofs == 0x33e447a0
- assert str(m.seq) == '03:03'
- assert m.data_len == 0x199
- assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
- assert m._forward_buffer==InverterIndMsg_81
- assert m._send_buffer==InverterRspMsg_81
+ assert m._forward_buffer==inverter_ind_msg+inverter_ind_msg_81
+ assert m._send_buffer==inverter_rsp_msg+inverter_rsp_msg_81
m._send_buffer = bytearray(0) # clear send buffer for next test
m._init_new_client_conn()
assert m._send_buffer==b''
m.close()
-def test_unkown_message(ConfigTsunInv1, UnknownMsg):
- ConfigTsunInv1
- m = MemoryStream(UnknownMsg, (0,))
+def test_unkown_message(config_tsun_inv1, unknown_msg):
+ config_tsun_inv1
+ m = MemoryStream(unknown_msg, (0,))
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
@@ -962,13 +905,13 @@ def test_unkown_message(ConfigTsunInv1, UnknownMsg):
assert m.data_len == 0x0a
assert m._recv_buffer==b''
assert m._send_buffer==b''
- assert m._forward_buffer==UnknownMsg
+ assert m._forward_buffer==unknown_msg
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_device_rsp(ConfigTsunInv1, DeviceRspMsg):
- ConfigTsunInv1
- m = MemoryStream(DeviceRspMsg, (0,), False)
+def test_device_rsp(config_tsun_inv1, device_rsp_msg):
+ config_tsun_inv1
+ m = MemoryStream(device_rsp_msg, (0,), False)
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
@@ -984,9 +927,9 @@ def test_device_rsp(ConfigTsunInv1, DeviceRspMsg):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_inverter_rsp(ConfigTsunInv1, InverterRspMsg):
- ConfigTsunInv1
- m = MemoryStream(InverterRspMsg, (0,), False)
+def test_inverter_rsp(config_tsun_inv1, inverter_rsp_msg):
+ config_tsun_inv1
+ m = MemoryStream(inverter_rsp_msg, (0,), False)
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
@@ -1002,27 +945,44 @@ def test_inverter_rsp(ConfigTsunInv1, InverterRspMsg):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_heartbeat_ind(ConfigTsunInv1, HeartbeatIndMsg, HeartbeatRspMsg):
- ConfigTsunInv1
- m = MemoryStream(HeartbeatIndMsg, (0,))
+def test_heartbeat_ind(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp_msg):
+ config_tsun_inv1
+ m = MemoryStream(heartbeat_ind_msg, (0,))
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.header_len==11
assert m.snr == 2070233889
- # assert m.unique_id == '2070233889'
assert m.control == 0x4710
assert str(m.seq) == '84:11' # value after sending response
assert m.data_len == 0x01
assert m._recv_buffer==b''
- assert m._send_buffer==HeartbeatRspMsg
- assert m._forward_buffer==HeartbeatIndMsg
+ assert m._send_buffer==heartbeat_rsp_msg
+ assert m._forward_buffer==heartbeat_ind_msg
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_heartbeat_rsp(ConfigTsunInv1, HeartbeatRspMsg):
- ConfigTsunInv1
- m = MemoryStream(HeartbeatRspMsg, (0,), False)
+def test_heartbeat_ind2(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp_msg):
+ config_tsun_inv1
+ m = MemoryStream(heartbeat_ind_msg, (0,))
+ m.no_forwarding = True
+ m.read() # read complete msg, and dispatch msg
+ assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
+ assert m.msg_count == 1
+ assert m.header_len==11
+ assert m.snr == 2070233889
+ assert m.control == 0x4710
+ assert str(m.seq) == '84:11' # value after sending response
+ assert m.data_len == 0x01
+ assert m._recv_buffer==b''
+ assert m._send_buffer==heartbeat_rsp_msg
+ assert m._forward_buffer==b''
+ assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
+ m.close()
+
+def test_heartbeat_rsp(config_tsun_inv1, heartbeat_rsp_msg):
+ config_tsun_inv1
+ m = MemoryStream(heartbeat_rsp_msg, (0,), False)
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
@@ -1038,33 +998,32 @@ def test_heartbeat_rsp(ConfigTsunInv1, HeartbeatRspMsg):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_sync_start_ind(ConfigTsunInv1, SyncStartIndMsg, SyncStartRspMsg, SyncStartFwdMsg):
- ConfigTsunInv1
- m = MemoryStream(SyncStartIndMsg, (0,))
+def test_sync_start_ind(config_tsun_inv1, sync_start_ind_msg, sync_start_rsp_msg, sync_start_fwd_msg):
+ config_tsun_inv1
+ m = MemoryStream(sync_start_ind_msg, (0,))
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.header_len==11
assert m.snr == 2070233889
- # assert m.unique_id == '2070233889'
assert m.control == 0x4310
assert str(m.seq) == '0d:0d' # value after sending response
assert m.data_len == 47
assert m._recv_buffer==b''
- assert m._send_buffer==SyncStartRspMsg
- assert m._forward_buffer==SyncStartIndMsg
+ assert m._send_buffer==sync_start_rsp_msg
+ assert m._forward_buffer==sync_start_ind_msg
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.seq.server_side = False # simulate forawding to TSUN cloud
m._update_header(m._forward_buffer)
assert str(m.seq) == '0d:0e' # value after forwarding indication
- assert m._forward_buffer==SyncStartFwdMsg
+ assert m._forward_buffer==sync_start_fwd_msg
m.close()
-def test_sync_start_rsp(ConfigTsunInv1, SyncStartRspMsg):
- ConfigTsunInv1
- m = MemoryStream(SyncStartRspMsg, (0,), False)
+def test_sync_start_rsp(config_tsun_inv1, sync_start_rsp_msg):
+ config_tsun_inv1
+ m = MemoryStream(sync_start_rsp_msg, (0,), False)
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
@@ -1080,27 +1039,26 @@ def test_sync_start_rsp(ConfigTsunInv1, SyncStartRspMsg):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_sync_end_ind(ConfigTsunInv1, SyncEndIndMsg, SyncEndRspMsg):
- ConfigTsunInv1
- m = MemoryStream(SyncEndIndMsg, (0,))
+def test_sync_end_ind(config_tsun_inv1, sync_end_ind_msg, sync_end_rsp_msg):
+ config_tsun_inv1
+ m = MemoryStream(sync_end_ind_msg, (0,))
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
assert m.header_len==11
assert m.snr == 2070233889
- # assert m.unique_id == '2070233889'
assert m.control == 0x4810
assert str(m.seq) == '07:07' # value after sending response
assert m.data_len == 60
assert m._recv_buffer==b''
- assert m._send_buffer==SyncEndRspMsg
- assert m._forward_buffer==SyncEndIndMsg
+ assert m._send_buffer==sync_end_rsp_msg
+ assert m._forward_buffer==sync_end_ind_msg
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_sync_end_rsp(ConfigTsunInv1, SyncEndRspMsg):
- ConfigTsunInv1
- m = MemoryStream(SyncEndRspMsg, (0,), False)
+def test_sync_end_rsp(config_tsun_inv1, sync_end_rsp_msg):
+ config_tsun_inv1
+ m = MemoryStream(sync_end_rsp_msg, (0,), False)
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
@@ -1116,9 +1074,9 @@ def test_sync_end_rsp(ConfigTsunInv1, SyncEndRspMsg):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_build_modell_600(ConfigTsunAllowAll, InverterIndMsg):
- ConfigTsunAllowAll
- m = MemoryStream(InverterIndMsg, (0,))
+def test_build_modell_600(config_tsun_allow_all, inverter_ind_msg):
+ config_tsun_allow_all
+ m = MemoryStream(inverter_ind_msg, (0,))
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
assert None == m.db.get_db_value(Register.RATED_POWER, None)
assert None == m.db.get_db_value(Register.INVERTER_TEMP, None)
@@ -1132,9 +1090,9 @@ def test_build_modell_600(ConfigTsunAllowAll, InverterIndMsg):
assert m._send_buffer==b''
m.close()
-def test_build_modell_1600(ConfigTsunAllowAll, InverterIndMsg1600):
- ConfigTsunAllowAll
- m = MemoryStream(InverterIndMsg1600, (0,))
+def test_build_modell_1600(config_tsun_allow_all, inverter_ind_msg1600):
+ config_tsun_allow_all
+ m = MemoryStream(inverter_ind_msg1600, (0,))
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
assert None == m.db.get_db_value(Register.RATED_POWER, None)
assert None == m.db.get_db_value(Register.INVERTER_TEMP, None)
@@ -1144,9 +1102,9 @@ def test_build_modell_1600(ConfigTsunAllowAll, InverterIndMsg1600):
assert 'TSOL-MS1600' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
m.close()
-def test_build_modell_1800(ConfigTsunAllowAll, InverterIndMsg1800):
- ConfigTsunAllowAll
- m = MemoryStream(InverterIndMsg1800, (0,))
+def test_build_modell_1800(config_tsun_allow_all, inverter_ind_msg1800):
+ config_tsun_allow_all
+ m = MemoryStream(inverter_ind_msg1800, (0,))
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
assert None == m.db.get_db_value(Register.RATED_POWER, None)
assert None == m.db.get_db_value(Register.INVERTER_TEMP, None)
@@ -1156,9 +1114,9 @@ def test_build_modell_1800(ConfigTsunAllowAll, InverterIndMsg1800):
assert 'TSOL-MS1800' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
m.close()
-def test_build_modell_2000(ConfigTsunAllowAll, InverterIndMsg2000):
- ConfigTsunAllowAll
- m = MemoryStream(InverterIndMsg2000, (0,))
+def test_build_modell_2000(config_tsun_allow_all, inverter_ind_msg2000):
+ config_tsun_allow_all
+ m = MemoryStream(inverter_ind_msg2000, (0,))
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
assert None == m.db.get_db_value(Register.RATED_POWER, None)
assert None == m.db.get_db_value(Register.INVERTER_TEMP, None)
@@ -1168,9 +1126,9 @@ def test_build_modell_2000(ConfigTsunAllowAll, InverterIndMsg2000):
assert 'TSOL-MS2000' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
m.close()
-def test_build_modell_800(ConfigTsunAllowAll, InverterIndMsg800):
- ConfigTsunAllowAll
- m = MemoryStream(InverterIndMsg800, (0,))
+def test_build_modell_800(config_tsun_allow_all, inverter_ind_msg800):
+ config_tsun_allow_all
+ m = MemoryStream(inverter_ind_msg800, (0,))
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
assert None == m.db.get_db_value(Register.RATED_POWER, None)
assert None == m.db.get_db_value(Register.INVERTER_TEMP, None)
@@ -1180,9 +1138,9 @@ def test_build_modell_800(ConfigTsunAllowAll, InverterIndMsg800):
assert 'TSOL-MSxx00' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
m.close()
-def test_build_logger_modell(ConfigTsunAllowAll, DeviceIndMsg):
- ConfigTsunAllowAll
- m = MemoryStream(DeviceIndMsg, (0,))
+def test_build_logger_modell(config_tsun_allow_all, device_ind_msg):
+ config_tsun_allow_all
+ m = MemoryStream(device_ind_msg, (0,))
assert 0 == m.db.get_db_value(Register.COLLECTOR_FW_VERSION, 0)
assert 'IGEN TECH' == m.db.get_db_value(Register.CHIP_TYPE, None)
assert None == m.db.get_db_value(Register.CHIP_MODEL, None)
@@ -1192,9 +1150,9 @@ def test_build_logger_modell(ConfigTsunAllowAll, DeviceIndMsg):
m.close()
def test_msg_iterator():
- m1 = SolarmanV5(server_side=True)
- m2 = SolarmanV5(server_side=True)
- m3 = SolarmanV5(server_side=True)
+ m1 = SolarmanV5(server_side=True, client_mode=False)
+ m2 = SolarmanV5(server_side=True, client_mode=False)
+ m3 = SolarmanV5(server_side=True, client_mode=False)
m3.close()
del m3
test1 = 0
@@ -1212,7 +1170,7 @@ def test_msg_iterator():
assert test2 == 1
def test_proxy_counter():
- m = SolarmanV5(server_side=True)
+ m = SolarmanV5(server_side=True, client_mode=False)
assert m.new_data == {}
m.db.stat['proxy']['Unknown_Msg'] = 0
Infos.new_stat_data['proxy'] = False
@@ -1230,39 +1188,41 @@ def test_proxy_counter():
m.close()
@pytest.mark.asyncio
-async def test_msg_build_modbus_req(ConfigTsunInv1, DeviceIndMsg, DeviceRspMsg, InverterIndMsg, InverterRspMsg, MsgModbusCmd):
- ConfigTsunInv1
- m = MemoryStream(DeviceIndMsg, (0,), True)
- m.append_msg(InverterIndMsg)
+async def test_msg_build_modbus_req(config_tsun_inv1, device_ind_msg, device_rsp_msg, inverter_ind_msg, inverter_rsp_msg, msg_modbus_cmd):
+ config_tsun_inv1
+ m = MemoryStream(device_ind_msg, (0,), True)
m.read()
assert m.control == 0x4110
assert str(m.seq) == '01:01'
- assert m._recv_buffer==InverterIndMsg # unhandled next message
- assert m._send_buffer==DeviceRspMsg
- assert m._forward_buffer==DeviceIndMsg
+ assert m._send_buffer==device_rsp_msg
+ assert m._forward_buffer==device_ind_msg
m._send_buffer = bytearray(0) # clear send buffer for next test
m._forward_buffer = bytearray(0) # clear send buffer for next test
await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0, logging.DEBUG)
- assert m._recv_buffer==InverterIndMsg # unhandled next message
assert 0 == m.send_msg_ofs
assert m._forward_buffer == b''
assert m.writer.sent_pdu == b'' # modbus command must be ignore, cause connection is still not up
assert m._send_buffer == b'' # modbus command must be ignore, cause connection is still not up
+ m.append_msg(inverter_ind_msg)
m.read()
assert m.control == 0x4210
assert str(m.seq) == '02:02'
+ assert m.msg_recvd[0]['control']==0x4110
+ assert m.msg_recvd[0]['seq']=='01:01'
+ assert m.msg_recvd[1]['control']==0x4210
+ assert m.msg_recvd[1]['seq']=='02:02'
assert m._recv_buffer==b''
- assert m._send_buffer==InverterRspMsg
- assert m._forward_buffer==InverterIndMsg
+ assert m._send_buffer==inverter_rsp_msg
+ assert m._forward_buffer==inverter_ind_msg
m._send_buffer = bytearray(0) # clear send buffer for next test
m._forward_buffer = bytearray(0) # clear send buffer for next test
await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0, logging.DEBUG)
assert 0 == m.send_msg_ofs
assert m._forward_buffer == b''
- assert m.writer.sent_pdu == MsgModbusCmd
+ assert m.writer.sent_pdu == msg_modbus_cmd
assert m._send_buffer == b''
m._send_buffer = bytearray(0) # clear send buffer for next test
@@ -1274,46 +1234,42 @@ async def test_msg_build_modbus_req(ConfigTsunInv1, DeviceIndMsg, DeviceRspMsg,
m.close()
@pytest.mark.asyncio
-async def test_AT_cmd(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, InverterIndMsg, InverterRspMsg, AtCommandIndMsg, AtCommandRspMsg):
- ConfigTsunAllowAll
- m = MemoryStream(DeviceIndMsg, (0,), True)
- m.append_msg(InverterIndMsg)
- m.append_msg(AtCommandRspMsg)
+async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inverter_ind_msg, inverter_rsp_msg, at_command_ind_msg, at_command_rsp_msg):
+ config_tsun_allow_all
+ m = MemoryStream(device_ind_msg, (0,), True)
m.read() # read device ind
assert m.control == 0x4110
assert str(m.seq) == '01:01'
- assert m._recv_buffer==InverterIndMsg + AtCommandRspMsg # unhandled next message
- assert m._send_buffer==DeviceRspMsg
- assert m._forward_buffer==DeviceIndMsg
+ assert m._send_buffer==device_rsp_msg
+ assert m._forward_buffer==device_ind_msg
m._send_buffer = bytearray(0) # clear send buffer for next test
m._forward_buffer = bytearray(0) # clear send buffer for next test
await m.send_at_cmd('AT+TIME=214028,1,60,120')
- assert m._recv_buffer==InverterIndMsg + AtCommandRspMsg # unhandled next message
assert m._send_buffer==b''
assert m._forward_buffer==b''
assert str(m.seq) == '01:01'
assert m.mqtt.key == ''
assert m.mqtt.data == ""
+ m.append_msg(inverter_ind_msg)
m.read() # read inverter ind
assert m.control == 0x4210
assert str(m.seq) == '02:02'
- assert m._recv_buffer==AtCommandRspMsg # unhandled next message
- assert m._send_buffer==InverterRspMsg
- assert m._forward_buffer==InverterIndMsg
+ assert m._send_buffer==inverter_rsp_msg
+ assert m._forward_buffer==inverter_ind_msg
m._send_buffer = bytearray(0) # clear send buffer for next test
m._forward_buffer = bytearray(0) # clear send buffer for next test
await m.send_at_cmd('AT+TIME=214028,1,60,120')
- assert m._recv_buffer==AtCommandRspMsg # unhandled next message
- assert m._send_buffer==AtCommandIndMsg
+ assert m._send_buffer==at_command_ind_msg
assert m._forward_buffer==b''
assert str(m.seq) == '02:03'
assert m.mqtt.key == ''
assert m.mqtt.data == ""
m._send_buffer = bytearray(0) # clear send buffer for next test
+ m.append_msg(at_command_rsp_msg)
m.read() # read at resp
assert m.control == 0x1510
assert str(m.seq) == '03:03'
@@ -1335,33 +1291,31 @@ async def test_AT_cmd(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, InverterIn
m.close()
@pytest.mark.asyncio
-async def test_AT_cmd_blocked(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, InverterIndMsg, InverterRspMsg, AtCommandIndMsg):
- ConfigTsunAllowAll
- m = MemoryStream(DeviceIndMsg, (0,), True)
- m.append_msg(InverterIndMsg)
+async def test_at_cmd_blocked(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inverter_ind_msg, inverter_rsp_msg, at_command_ind_msg):
+ config_tsun_allow_all
+ m = MemoryStream(device_ind_msg, (0,), True)
m.read()
assert m.control == 0x4110
assert str(m.seq) == '01:01'
- assert m._recv_buffer==InverterIndMsg # unhandled next message
- assert m._send_buffer==DeviceRspMsg
- assert m._forward_buffer==DeviceIndMsg
+ assert m._send_buffer==device_rsp_msg
+ assert m._forward_buffer==device_ind_msg
m._send_buffer = bytearray(0) # clear send buffer for next test
m._forward_buffer = bytearray(0) # clear send buffer for next test
await m.send_at_cmd('AT+WEBU')
- assert m._recv_buffer==InverterIndMsg # unhandled next message
assert m._send_buffer==b''
assert m._forward_buffer==b''
assert str(m.seq) == '01:01'
assert m.mqtt.key == ''
assert m.mqtt.data == ""
+ m.append_msg(inverter_ind_msg)
m.read()
assert m.control == 0x4210
assert str(m.seq) == '02:02'
assert m._recv_buffer==b''
- assert m._send_buffer==InverterRspMsg
- assert m._forward_buffer==InverterIndMsg
+ assert m._send_buffer==inverter_rsp_msg
+ assert m._forward_buffer==inverter_ind_msg
m._send_buffer = bytearray(0) # clear send buffer for next test
m._forward_buffer = bytearray(0) # clear send buffer for next test
@@ -1375,9 +1329,9 @@ async def test_AT_cmd_blocked(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, In
assert m.mqtt.data == "'AT+WEBU' is forbidden"
m.close()
-def test_AT_cmd_ind(ConfigTsunInv1, AtCommandIndMsg):
- ConfigTsunInv1
- m = MemoryStream(AtCommandIndMsg, (0,), False)
+def test_at_cmd_ind(config_tsun_inv1, at_command_ind_msg):
+ config_tsun_inv1
+ m = MemoryStream(at_command_ind_msg, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['AT_Command'] = 0
m.db.stat['proxy']['AT_Command_Blocked'] = 0
@@ -1387,22 +1341,21 @@ def test_AT_cmd_ind(ConfigTsunInv1, AtCommandIndMsg):
assert m.msg_count == 1
assert m.header_len==11
assert m.snr == 2070233889
- # assert m.unique_id == '2070233889'
assert m.control == 0x4510
assert str(m.seq) == '03:02'
assert m.data_len == 39
assert m._recv_buffer==b''
assert m._send_buffer==b''
- assert m._forward_buffer==AtCommandIndMsg
+ assert m._forward_buffer==at_command_ind_msg
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
assert m.db.stat['proxy']['AT_Command'] == 1
assert m.db.stat['proxy']['AT_Command_Blocked'] == 0
assert m.db.stat['proxy']['Modbus_Command'] == 0
m.close()
-def test_AT_cmd_ind_block(ConfigTsunInv1, AtCommandIndMsgBlock):
- ConfigTsunInv1
- m = MemoryStream(AtCommandIndMsgBlock, (0,), False)
+def test_at_cmd_ind_block(config_tsun_inv1, at_command_ind_msg_block):
+ config_tsun_inv1
+ m = MemoryStream(at_command_ind_msg_block, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['AT_Command'] = 0
m.db.stat['proxy']['AT_Command_Blocked'] = 0
@@ -1412,7 +1365,6 @@ def test_AT_cmd_ind_block(ConfigTsunInv1, AtCommandIndMsgBlock):
assert m.msg_count == 1
assert m.header_len==11
assert m.snr == 2070233889
- # assert m.unique_id == '2070233889'
assert m.control == 0x4510
assert str(m.seq) == '03:02'
assert m.data_len == 23
@@ -1425,9 +1377,9 @@ def test_AT_cmd_ind_block(ConfigTsunInv1, AtCommandIndMsgBlock):
assert m.db.stat['proxy']['Modbus_Command'] == 0
m.close()
-def test_msg_at_command_rsp1(ConfigTsunInv1, AtCommandRspMsg):
- ConfigTsunInv1
- m = MemoryStream(AtCommandRspMsg)
+def test_msg_at_command_rsp1(config_tsun_inv1, at_command_rsp_msg):
+ config_tsun_inv1
+ m = MemoryStream(at_command_rsp_msg)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
m.forward_at_cmd_resp = True
@@ -1438,15 +1390,15 @@ def test_msg_at_command_rsp1(ConfigTsunInv1, AtCommandRspMsg):
assert str(m.seq) == '03:03'
assert m.header_len==11
assert m.data_len==17
- assert m._forward_buffer==AtCommandRspMsg
+ assert m._forward_buffer==at_command_rsp_msg
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['Modbus_Command'] == 0
m.close()
-def test_msg_at_command_rsp2(ConfigTsunInv1, AtCommandRspMsg):
- ConfigTsunInv1
- m = MemoryStream(AtCommandRspMsg)
+def test_msg_at_command_rsp2(config_tsun_inv1, at_command_rsp_msg):
+ config_tsun_inv1
+ m = MemoryStream(at_command_rsp_msg)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
m.forward_at_cmd_resp = False
@@ -1463,12 +1415,12 @@ def test_msg_at_command_rsp2(ConfigTsunInv1, AtCommandRspMsg):
assert m.db.stat['proxy']['Modbus_Command'] == 0
m.close()
-def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd, MsgModbusCmdFwd):
- ConfigTsunInv1
+def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd, msg_modbus_cmd_fwd):
+ config_tsun_inv1
m = MemoryStream(b'')
m.snr = get_sn_int()
m.state = State.up
- c = m.createClientStream(MsgModbusCmd)
+ c = m.createClientStream(msg_modbus_cmd)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['AT_Command'] = 0
@@ -1483,19 +1435,19 @@ def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd, MsgModbusCmdFwd):
assert c.data_len==23
assert c._forward_buffer==b''
assert c._send_buffer==b''
- assert m.writer.sent_pdu == MsgModbusCmdFwd
+ assert m.writer.sent_pdu == msg_modbus_cmd_fwd
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['AT_Command'] == 0
assert m.db.stat['proxy']['Modbus_Command'] == 1
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_msg_modbus_req2(ConfigTsunInv1, MsgModbusCmdCrcErr):
- ConfigTsunInv1
+def test_msg_modbus_req2(config_tsun_inv1, msg_modbus_cmd_crc_err):
+ config_tsun_inv1
m = MemoryStream(b'')
m.snr = get_sn_int()
m.state = State.up
- c = m.createClientStream(MsgModbusCmdCrcErr)
+ c = m.createClientStream(msg_modbus_cmd_crc_err)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['AT_Command'] = 0
@@ -1517,9 +1469,9 @@ def test_msg_modbus_req2(ConfigTsunInv1, MsgModbusCmdCrcErr):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
m.close()
-def test_msg_unknown_cmd_req(ConfigTsunInv1, MsgUnknownCmd):
- ConfigTsunInv1
- m = MemoryStream(MsgUnknownCmd, (0,), False)
+def test_msg_unknown_cmd_req(config_tsun_inv1, msg_unknown_cmd):
+ config_tsun_inv1
+ m = MemoryStream(msg_unknown_cmd, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['AT_Command'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
@@ -1531,7 +1483,7 @@ def test_msg_unknown_cmd_req(ConfigTsunInv1, MsgUnknownCmd):
assert str(m.seq) == '03:02'
assert m.header_len==11
assert m.data_len==23
- assert m._forward_buffer==MsgUnknownCmd
+ assert m._forward_buffer==msg_unknown_cmd
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['AT_Command'] == 0
@@ -1539,10 +1491,10 @@ def test_msg_unknown_cmd_req(ConfigTsunInv1, MsgUnknownCmd):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_msg_modbus_rsp1(ConfigTsunInv1, MsgModbusRsp):
+def test_msg_modbus_rsp1(config_tsun_inv1, msg_modbus_rsp):
'''Modbus response without a valid Modbus request must be dropped'''
- ConfigTsunInv1
- m = MemoryStream(MsgModbusRsp)
+ config_tsun_inv1
+ m = MemoryStream(msg_modbus_rsp)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
m.read() # read complete msg, and dispatch msg
@@ -1558,11 +1510,10 @@ def test_msg_modbus_rsp1(ConfigTsunInv1, MsgModbusRsp):
assert m.db.stat['proxy']['Modbus_Command'] == 0
m.close()
-def test_msg_modbus_rsp2(ConfigTsunInv1, MsgModbusRsp):
+def test_msg_modbus_rsp2(config_tsun_inv1, msg_modbus_rsp):
'''Modbus response with a valid Modbus request must be forwarded'''
- ConfigTsunInv1
- m = MemoryStream(MsgModbusRsp)
- m.append_msg(MsgModbusRsp)
+ config_tsun_inv1 # setup config structure
+ m = MemoryStream(msg_modbus_rsp)
m.mb.rsp_handler = m._SolarmanV5__forward_msg
m.mb.last_addr = 1
@@ -1571,38 +1522,36 @@ def test_msg_modbus_rsp2(ConfigTsunInv1, MsgModbusRsp):
m.mb.last_reg = 0x3008
m.mb.req_pend = True
m.mb.err = 0
- # assert m.db.db == {'inverter': {'Manufacturer': 'TSUN', 'Equipment_Model': 'TSOL-MSxx00'}}
m.new_data['inverter'] = False
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.mb.err == 0
assert m.msg_count == 1
- assert m._forward_buffer==MsgModbusRsp
+ assert m._forward_buffer==msg_modbus_rsp
assert m._send_buffer==b''
- # assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
assert m.db.get_db_value(Register.VERSION) == 'V4.0.10'
assert m.new_data['inverter'] == True
m.new_data['inverter'] = False
m.mb.req_pend = True
+ m._forward_buffer = bytearray()
+ m.append_msg(msg_modbus_rsp)
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.mb.err == 0
assert m.msg_count == 2
- assert m._forward_buffer==MsgModbusRsp
+ assert m._forward_buffer==msg_modbus_rsp
assert m._send_buffer==b''
- # assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
assert m.db.get_db_value(Register.VERSION) == 'V4.0.10'
assert m.new_data['inverter'] == False
m.close()
-def test_msg_modbus_rsp3(ConfigTsunInv1, MsgModbusRsp):
+def test_msg_modbus_rsp3(config_tsun_inv1, msg_modbus_rsp):
'''Modbus response with a valid Modbus request must be forwarded'''
- ConfigTsunInv1
- m = MemoryStream(MsgModbusRsp)
- m.append_msg(MsgModbusRsp)
+ config_tsun_inv1
+ m = MemoryStream(msg_modbus_rsp)
m.mb.rsp_handler = m._SolarmanV5__forward_msg
m.mb.last_addr = 1
@@ -1611,35 +1560,34 @@ def test_msg_modbus_rsp3(ConfigTsunInv1, MsgModbusRsp):
m.mb.last_reg = 0x3008
m.mb.req_pend = True
m.mb.err = 0
- # assert m.db.db == {'inverter': {'Manufacturer': 'TSUN', 'Equipment_Model': 'TSOL-MSxx00'}}
m.new_data['inverter'] = False
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.mb.err == 0
assert m.msg_count == 1
- assert m._forward_buffer==MsgModbusRsp
+ assert m._forward_buffer==msg_modbus_rsp
assert m._send_buffer==b''
- # assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
assert m.db.get_db_value(Register.VERSION) == 'V4.0.10'
assert m.new_data['inverter'] == True
m.new_data['inverter'] = False
+ m._forward_buffer = bytearray()
+ m.append_msg(msg_modbus_rsp)
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.mb.err == 5
assert m.msg_count == 2
- assert m._forward_buffer==MsgModbusRsp
+ assert m._forward_buffer==b''
assert m._send_buffer==b''
- # assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
assert m.db.get_db_value(Register.VERSION) == 'V4.0.10'
assert m.new_data['inverter'] == False
m.close()
-def test_msg_unknown_rsp(ConfigTsunInv1, MsgUnknownCmdRsp):
- ConfigTsunInv1
- m = MemoryStream(MsgUnknownCmdRsp)
+def test_msg_unknown_rsp(config_tsun_inv1, msg_unknown_cmd_rsp):
+ config_tsun_inv1
+ m = MemoryStream(msg_unknown_cmd_rsp)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
m.read() # read complete msg, and dispatch msg
@@ -1649,15 +1597,15 @@ def test_msg_unknown_rsp(ConfigTsunInv1, MsgUnknownCmdRsp):
assert str(m.seq) == '03:03'
assert m.header_len==11
assert m.data_len==59
- assert m._forward_buffer==MsgUnknownCmdRsp
+ assert m._forward_buffer==msg_unknown_cmd_rsp
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['Modbus_Command'] == 0
m.close()
-def test_msg_modbus_invalid(ConfigTsunInv1, MsgModbusInvalid):
- ConfigTsunInv1
- m = MemoryStream(MsgModbusInvalid, (0,), False)
+def test_msg_modbus_invalid(config_tsun_inv1, msg_modbus_invalid):
+ config_tsun_inv1
+ m = MemoryStream(msg_modbus_invalid, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
m.read() # read complete msg, and dispatch msg
@@ -1669,10 +1617,10 @@ def test_msg_modbus_invalid(ConfigTsunInv1, MsgModbusInvalid):
assert m.db.stat['proxy']['Modbus_Command'] == 0
m.close()
-def test_msg_modbus_fragment(ConfigTsunInv1, MsgModbusRsp):
- ConfigTsunInv1
+def test_msg_modbus_fragment(config_tsun_inv1, msg_modbus_rsp):
+ config_tsun_inv1
# receive more bytes than expected (7 bytes from the next msg)
- m = MemoryStream(MsgModbusRsp+b'\x00\x00\x00\x45\x10\x52\x31', (0,))
+ m = MemoryStream(msg_modbus_rsp+b'\x00\x00\x00\x45\x10\x52\x31', (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
m.mb.rsp_handler = m._SolarmanV5__forward_msg
@@ -1686,36 +1634,83 @@ def test_msg_modbus_fragment(ConfigTsunInv1, MsgModbusRsp):
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
- assert m._forward_buffer==MsgModbusRsp
+ assert m._forward_buffer==msg_modbus_rsp
assert m._send_buffer == b''
assert m.mb.err == 0
assert m.modbus_elms == 20-1 # register 0x300d is unknown, so one value can't be mapped
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['Modbus_Command'] == 0
m.close()
-'''
-def test_zombie_conn(ConfigTsunInv1, MsgInverterInd):
- ConfigTsunInv1
- tracer.setLevel(logging.DEBUG)
- m1 = MemoryStream(MsgInverterInd, (0,))
- m2 = MemoryStream(MsgInverterInd, (0,))
- m3 = MemoryStream(MsgInverterInd, (0,))
- assert m1.state == m1.State.init
- assert m2.state == m2.State.init
- assert m3.state == m3.State.init
- m1.read() # read complete msg, and set unique_id
- assert m1.state == m1.State.init
- assert m2.state == m2.State.init
- assert m3.state == m3.State.init
- m2.read() # read complete msg, and set unique_id
- assert m1.state == m1.State.closed
- assert m2.state == m2.State.init
- assert m3.state == m3.State.init
- m3.read() # read complete msg, and set unique_id
- assert m1.state == m1.State.closed
- assert m2.state == m2.State.closed
- assert m3.state == m3.State.init
- m1.close()
- m2.close()
- m3.close()
-'''
\ No newline at end of file
+
+@pytest.mark.asyncio
+async def test_modbus_polling(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp_msg):
+ config_tsun_inv1
+ assert asyncio.get_running_loop()
+ m = MemoryStream(heartbeat_ind_msg, (0,))
+ assert asyncio.get_running_loop() == m.mb_timer.loop
+ m.db.stat['proxy']['Unknown_Ctrl'] = 0
+ assert m.mb_timer.tim == None
+ m.read() # read complete msg, and dispatch msg
+ assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
+ assert m.msg_count == 1
+ assert m.header_len==11
+ assert m.snr == 2070233889
+ assert m.control == 0x4710
+ assert str(m.seq) == '84:11' # value after sending response
+ assert m.data_len == 0x01
+ assert m._recv_buffer==b''
+ assert m._send_buffer==heartbeat_rsp_msg
+ assert m._forward_buffer==heartbeat_ind_msg
+ assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
+
+ m._send_buffer = bytearray(0) # clear send buffer for next test
+ assert m.state == State.up
+ assert m.mb_timeout == 0.5
+ assert next(m.mb_timer.exp_count) == 0
+
+ await asyncio.sleep(0.5)
+ assert m.writer.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x12\x84!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x30\x00\x000J\xde\x86\x15')
+ assert m._send_buffer==b''
+
+ await asyncio.sleep(0.5)
+ assert m.writer.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x13\x84!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x30\x00\x000J\xde\x87\x15')
+ assert m._send_buffer==b''
+ m.state = State.closed
+ m.writer.sent_pdu = bytearray()
+ await asyncio.sleep(0.5)
+ assert m.writer.sent_pdu==bytearray(b'')
+ assert m._send_buffer==b''
+ assert next(m.mb_timer.exp_count) == 4
+ m.close()
+
+@pytest.mark.asyncio
+async def test_start_client_mode(config_tsun_inv1):
+ config_tsun_inv1
+ assert asyncio.get_running_loop()
+ m = MemoryStream(b'')
+ assert m.state == State.init
+ assert m.no_forwarding == False
+ assert m.mb_timer.tim == None
+ assert asyncio.get_running_loop() == m.mb_timer.loop
+ await m.send_start_cmd(get_sn_int(), '192.168.1.1', m.mb_start_timeout)
+ assert m.writer.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x01\x00!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x030\x00\x000J\xde\xf1\x15')
+ assert m.db.get_db_value(Register.IP_ADDRESS) == '192.168.1.1'
+ assert m.db.get_db_value(Register.POLLING_INTERVAL) == 0.5
+ assert m.db.get_db_value(Register.HEARTBEAT_INTERVAL) == 120
+
+ assert m.state == State.up
+ assert m.no_forwarding == True
+
+ assert m._send_buffer==b''
+ assert m.mb_timeout == 0.5
+ assert next(m.mb_timer.exp_count) == 0
+
+ await asyncio.sleep(0.5)
+ assert m.writer.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x02\x00!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x030\x00\x000J\xde\xf2\x15')
+ assert m._send_buffer==b''
+
+ await asyncio.sleep(0.5)
+ assert m.writer.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x03\x00!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x030\x00\x000J\xde\xf3\x15')
+ assert m._send_buffer==b''
+ assert next(m.mb_timer.exp_count) == 3
+ m.close()
diff --git a/app/tests/test_talent.py b/app/tests/test_talent.py
index f8efca0..941add2 100644
--- a/app/tests/test_talent.py
+++ b/app/tests/test_talent.py
@@ -1,5 +1,5 @@
# test_with_pytest.py
-import pytest, logging
+import pytest, logging, asyncio
from app.src.gen3.talent import Talent, Control
from app.src.config import Config
from app.src.infos import Infos, Register
@@ -26,7 +26,9 @@ class MemoryStream(Talent):
def __init__(self, msg, chunks = (0,), server_side: bool = True):
super().__init__(server_side)
if server_side:
- self.mb.timeout = 1 # overwrite for faster testing
+ self.mb.timeout = 0.4 # overwrite for faster testing
+ self.mb_start_timeout = 0.5
+ self.mb_timeout = 0.5
self.writer = Writer()
self.__msg = msg
self.__msg_len = len(msg)
@@ -37,6 +39,7 @@ class MemoryStream(Talent):
self.addr = 'Test: SrvSide'
self.send_msg_ofs = 0
self.test_exception_async_write = False
+ self.msg_recvd = []
def append_msg(self, msg):
self.__msg += msg
@@ -46,34 +49,45 @@ class MemoryStream(Talent):
copied_bytes = 0
try:
if (self.__offs < self.__msg_len):
- len = self.__chunks[self.__chunk_idx]
+ chunk_len = self.__chunks[self.__chunk_idx]
self.__chunk_idx += 1
- if len!=0:
- self._recv_buffer += self.__msg[self.__offs:len]
- copied_bytes = len - self.__offs
- self.__offs = len
+ if chunk_len!=0:
+ self._recv_buffer += self.__msg[self.__offs:chunk_len]
+ copied_bytes = chunk_len - self.__offs
+ self.__offs = chunk_len
else:
self._recv_buffer += self.__msg[self.__offs:]
copied_bytes = self.__msg_len - self.__offs
self.__offs = self.__msg_len
- except:
- pass
+ except Exception:
+ pass # ignore exceptions here
return copied_bytes
def _timestamp(self):
- # return 1700260990000
return 1691246944000
+
+ def _utc(self):
+ return 1691239744.0
def createClientStream(self, msg, chunks = (0,)):
c = MemoryStream(msg, chunks, False)
- self.remoteStream = c
- c. remoteStream = self
+ self.remote_stream = c
+ c. remote_stream = self
return c
def _Talent__flush_recv_msg(self) -> None:
+ self.msg_recvd.append(
+ {
+ 'ctrl': int(self.ctrl),
+ 'msg_id': self.msg_id,
+ 'header_len': self.header_len,
+ 'data_len': self.data_len
+ }
+ )
+
super()._Talent__flush_recv_msg()
+
self.msg_count += 1
- return
async def async_write(self, headline=''):
if self.test_exception_async_write:
@@ -82,49 +96,49 @@ class MemoryStream(Talent):
@pytest.fixture
-def MsgContactInfo(): # Contact Info message
+def msg_contact_info(): # Contact Info message
Config.config = {'tsun':{'enabled': True}}
return b'\x00\x00\x00\x2c\x10R170000000000001\x91\x00\x08solarhub\x0fsolarhub\x40123456'
@pytest.fixture
-def MsgContactInfo_LongId(): # Contact Info message with longer ID
+def msg_contact_info_long_id(): # Contact Info message with longer ID
Config.config = {'tsun':{'enabled': True}}
return b'\x00\x00\x00\x2d\x11R1700000000000011\x91\x00\x08solarhub\x0fsolarhub\x40123456'
@pytest.fixture
-def Msg2ContactInfo(): # two Contact Info messages
+def msg2_contact_info(): # two Contact Info messages
return b'\x00\x00\x00\x2c\x10R170000000000001\x91\x00\x08solarhub\x0fsolarhub\x40123456\x00\x00\x00\x2c\x10R170000000000002\x91\x00\x08solarhub\x0fsolarhub\x40123456'
@pytest.fixture
-def MsgContactResp(): # Contact Response message
+def msg_contact_rsp(): # Contact Response message
return b'\x00\x00\x00\x14\x10R170000000000001\x91\x00\x01'
@pytest.fixture
-def MsgContactResp2(): # Contact Response message
+def msg_contact_rsp2(): # Contact Response message
return b'\x00\x00\x00\x14\x10R170000000000002\x91\x00\x01'
@pytest.fixture
-def MsgContactInvalid(): # Contact Response message
+def msg_contact_invalid(): # Contact Response message
return b'\x00\x00\x00\x14\x10R170000000000001\x93\x00\x01'
@pytest.fixture
-def MsgGetTime(): # Get Time Request message
+def msg_get_time(): # Get Time Request message
return b'\x00\x00\x00\x13\x10R170000000000001\x91\x22'
@pytest.fixture
-def MsgTimeResp(): # Get Time Resonse message
+def msg_time_rsp(): # Get Time Resonse message
return b'\x00\x00\x00\x1b\x10R170000000000001\x91\x22\x00\x00\x01\x89\xc6\x63\x4d\x80'
@pytest.fixture
-def MsgTimeRespInv(): # Get Time Resonse message
+def msg_time_rsp_inv(): # Get Time Resonse message
return b'\x00\x00\x00\x17\x10R170000000000001\x91\x22\x00\x00\x01\x89'
@pytest.fixture
-def MsgTimeInvalid(): # Get Time Request message
+def msg_time_invalid(): # Get Time Request message
return b'\x00\x00\x00\x13\x10R170000000000001\x94\x22'
@pytest.fixture
-def MsgControllerInd(): # Data indication from the controller
+def msg_controller_ind(): # Data indication from the controller
msg = b'\x00\x00\x01\x2f\x10R170000000000001\x91\x71\x0e\x10\x00\x00\x10R170000000000001'
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'
@@ -136,7 +150,7 @@ def MsgControllerInd(): # Data indication from the controller
return msg
@pytest.fixture
-def MsgControllerIndTsOffs(): # Data indication from the controller - offset 0x1000
+def msg_controller_ind_ts_offs(): # Data indication from the controller - offset 0x1000
msg = b'\x00\x00\x01\x2f\x10R170000000000001\x91\x71\x0e\x10\x00\x00\x10R170000000000001'
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x45\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'
@@ -148,15 +162,15 @@ def MsgControllerIndTsOffs(): # Data indication from the controller - offset 0x1
return msg
@pytest.fixture
-def MsgControllerAck(): # Get Time Request message
+def msg_controller_ack(): # Get Time Request message
return b'\x00\x00\x00\x14\x10R170000000000001\x99\x71\x01'
@pytest.fixture
-def MsgControllerInvalid(): # Get Time Request message
+def msg_controller_invalid(): # Get Time Request message
return b'\x00\x00\x00\x14\x10R170000000000001\x92\x71\x01'
@pytest.fixture
-def MsgInverterInd(): # Data indication from the controller
+def msg_inverter_ind(): # Data indication from the controller
msg = b'\x00\x00\x00\x8b\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001'
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'
@@ -164,7 +178,7 @@ def MsgInverterInd(): # Data indication from the controller
return msg
@pytest.fixture
-def MsgInverterIndTsOffs(): # Data indication from the controller + offset 256
+def msg_inverter_ind_ts_offs(): # Data indication from the controller + offset 256
msg = b'\x00\x00\x00\x8b\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001'
msg += b'\x01\x00\x00\x01\x89\xc6\x63\x62\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'
@@ -172,7 +186,44 @@ def MsgInverterIndTsOffs(): # Data indication from the controller + offset 256
return msg
@pytest.fixture
-def MsgInverterIndNew(): # Data indication from DSP V5.0.17
+def msg_inverter_ind2(): # Data indication from the controller
+ msg = b'\x00\x00\x05\x02\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001'
+ msg += b'\x01\x00\x00\x01\x89\xc6\x63\x61\x08'
+ msg += b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00'
+ msg += b'\x00\x00\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00'
+ msg += b'\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53'
+ msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00\x00\x00\x01\xf5\x53\x00\x00\x00\x00'
+ msg += b'\x01\xf6\x53\x00\x00\x00\x00\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00\x00\x00'
+ msg += b'\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00'
+ msg += b'\x00\x00\x02\x02\x53\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00\x00\x00\x02\x5a'
+ msg += b'\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02'
+ msg += b'\x60\x53\x00\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02\x65\x53\x00\x00\x00\x00'
+ msg += b'\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00'
+ msg += b'\x00\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3\x53\x00\x00\x00\x00\x02\xc4\x53'
+ msg += b'\x00\x00\x00\x00\x02\xc5\x53\x00\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02\xca'
+ msg += b'\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53\x00\x00\x00\x00\x03\x20\x53\x00\x00\x00\x00\x03\x84\x53\x50\x11\x00\x00\x03\xe8\x46\x43\x61\x66\x66\x00'
+ msg += b'\x00\x04\x4c\x46\x3e\xeb\x85\x1f\x00\x00\x04\xb0\x46\x42\x48\x14\x7b\x00\x00\x05\x14\x53\x00\x17\x00\x00\x05\x78\x53\x00\x00\x00\x00\x05\xdc\x53\x02\x58\x00\x00\x06'
+ msg += b'\x40\x46\x42\xd3\x66\x66\x00\x00\x06\xa4\x46\x42\x06\x66\x66\x00\x00\x07\x08\x46\x3f\xf4\x7a\xe1\x00\x00\x07\x6c\x46\x42\x81\x00\x00\x00\x00\x07\xd0\x46\x42\x06\x00'
+ msg += b'\x00\x00\x00\x08\x34\x46\x3f\xae\x14\x7b\x00\x00\x08\x98\x46\x42\x36\xcc\xcd\x00\x00\x08\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x00\x00\x00\x00\x00\x00\x09\xc4'
+ msg += b'\x46\x00\x00\x00\x00\x00\x00\x0a\x28\x46\x00\x00\x00\x00\x00\x00\x0a\x8c\x46\x00\x00\x00\x00\x00\x00\x0a\xf0\x46\x00\x00\x00\x00\x00\x00\x0b\x54\x46\x3f\xd9\x99\x9a'
+ msg += b'\x00\x00\x0b\xb8\x46\x41\x8a\xe1\x48\x00\x00\x0c\x1c\x46\x3f\x8a\x3d\x71\x00\x00\x0c\x80\x46\x41\x1b\xd7\x0a\x00\x00\x0c\xe4\x46\x3f\x1e\xb8\x52\x00\x00\x0d\x48\x46'
+ msg += b'\x40\xf3\xd7\x0a\x00\x00\x0d\xac\x46\x00\x00\x00\x00\x00\x00\x0e\x10\x46\x00\x00\x00\x00\x00\x00\x0e\x74\x46\x00\x00\x00\x00\x00\x00\x0e\xd8\x46\x00\x00\x00\x00\x00'
+ msg += b'\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0\x53\x00\x00\x00\x00\x10\x04\x53\x55\xaa\x00\x00\x10\x68\x53\x00\x00\x00\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00'
+ msg += b'\x00\x00\x11\x94\x53\x00\x00\x00\x00\x11\xf8\x53\xff\xff\x00\x00\x12\x5c\x53\xff\xff\x00\x00\x12\xc0\x53\x00\x02\x00\x00\x13\x24\x53\xff\xff\x00\x00\x13\x88\x53\xff'
+ msg += b'\xff\x00\x00\x13\xec\x53\xff\xff\x00\x00\x14\x50\x53\xff\xff\x00\x00\x14\xb4\x53\xff\xff\x00\x00\x15\x18\x53\xff\xff\x00\x00\x15\x7c\x53\x00\x00\x00\x00\x27\x10\x53'
+ msg += b'\x00\x02\x00\x00\x27\x74\x53\x00\x3c\x00\x00\x27\xd8\x53\x00\x68\x00\x00\x28\x3c\x53\x05\x00\x00\x00\x28\xa0\x46\x43\x79\x00\x00\x00\x00\x29\x04\x46\x43\x48\x00\x00'
+ msg += b'\x00\x00\x29\x68\x46\x42\x48\x33\x33\x00\x00\x29\xcc\x46\x42\x3e\x3d\x71\x00\x00\x2a\x30\x53\x00\x01\x00\x00\x2a\x94\x46\x43\x37\x00\x00\x00\x00\x2a\xf8\x46\x42\xce'
+ msg += b'\x00\x00\x00\x00\x2b\x5c\x53\x00\x96\x00\x00\x2b\xc0\x53\x00\x10\x00\x00\x2c\x24\x46\x43\x90\x00\x00\x00\x00\x2c\x88\x46\x43\x95\x00\x00\x00\x00\x2c\xec\x53\x00\x06'
+ msg += b'\x00\x00\x2d\x50\x53\x00\x06\x00\x00\x2d\xb4\x46\x43\x7d\x00\x00\x00\x00\x2e\x18\x46\x42\x3d\xeb\x85\x00\x00\x2e\x7c\x46\x42\x3d\xeb\x85\x00\x00\x2e\xe0\x53\x00\x03'
+ msg += b'\x00\x00\x2f\x44\x53\x00\x03\x00\x00\x2f\xa8\x46\x42\x4d\xeb\x85\x00\x00\x30\x0c\x46\x42\x4d\xeb\x85\x00\x00\x30\x70\x53\x00\x03\x00\x00\x30\xd4\x53\x00\x03\x00\x00'
+ msg += b'\x31\x38\x46\x42\x08\x00\x00\x00\x00\x31\x9c\x53\x00\x05\x00\x00\x32\x00\x53\x04\x00\x00\x00\x32\x64\x53\x00\x01\x00\x00\x32\xc8\x53\x13\x9c\x00\x00\x33\x2c\x53\x0f'
+ msg += b'\xa0\x00\x00\x33\x90\x53\x00\x4f\x00\x00\x33\xf4\x53\x00\x66\x00\x00\x34\x58\x53\x03\xe8\x00\x00\x34\xbc\x53\x04\x00\x00\x00\x35\x20\x53\x00\x00\x00\x00\x35\x84\x53'
+ msg += b'\x00\x00\x00\x00\x35\xe8\x53\x00\x00\x00\x00\x36\x4c\x53\x00\x00\x00\x01\x38\x80\x53\x00\x02\x00\x01\x38\x81\x53\x00\x01\x00\x01\x38\x82\x53\x00\x01\x00\x01\x38\x83'
+ msg += b'\x53\x00\x00'
+ return msg
+
+@pytest.fixture
+def msg_inverter_ind_new(): # Data indication from DSP V5.0.17
msg = b'\x00\x00\x04\xa0\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001'
msg += b'\x01\x00\x00\x01'
msg += b'\x90\x31\x4d\x68\x78\x00\x00\x00\xa3\x00\x00\x00\x00\x53\x00\x00'
@@ -250,31 +301,73 @@ def MsgInverterIndNew(): # Data indication from DSP V5.0.17
return msg
@pytest.fixture
-def MsgInverterAck(): # Get Time Request message
+def msg_inverter_ind_0w(): # Data indication with 0.5W grid output
+ msg = b'\x00\x00\x05\x02\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001'
+ msg += b'\x01\x00\x00\x01'
+ msg += b'\x90\x31\x4d\x68\x78'
+ msg += b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00'
+ msg += b'\x00\x00\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00'
+ msg += b'\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53'
+ msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00\x00\x00\x01\xf5\x53\x00\x00\x00\x00'
+ msg += b'\x01\xf6\x53\x00\x00\x00\x00\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00\x00\x00'
+ msg += b'\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00'
+ msg += b'\x00\x00\x02\x02\x53\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00\x00\x00\x02\x5a'
+ msg += b'\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02'
+ msg += b'\x60\x53\x00\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02\x65\x53\x00\x00\x00\x00'
+ msg += b'\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00'
+ msg += b'\x00\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3\x53\x00\x00\x00\x00\x02\xc4\x53'
+ msg += b'\x00\x00\x00\x00\x02\xc5\x53\x00\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02\xca'
+ msg += b'\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53\x00\x00\x00\x00\x03\x20\x53\x00\x00\x00\x00\x03\x84\x53\x50\x11\x00\x00\x03\xe8\x46\x43\x61\x66\x66\x00'
+ msg += b'\x00\x04\x4c\x46\x3e\xeb\x85\x1f\x00\x00\x04\xb0\x46\x42\x48\x14\x7b\x00\x00\x05\x14\x53\x00\x17\x00\x00\x05\x78\x53\x00\x00\x00\x00\x05\xdc\x53\x02\x58\x00\x00\x06'
+ msg += b'\x40\x46\x3f\x00\x00\x00\x00\x00\x06\xa4\x46\x42\x06\x66\x66\x00\x00\x07\x08\x46\x3f\xf4\x7a\xe1\x00\x00\x07\x6c\x46\x42\x81\x00\x00\x00\x00\x07\xd0\x46\x42\x06\x00'
+ msg += b'\x00\x00\x00\x08\x34\x46\x3f\xae\x14\x7b\x00\x00\x08\x98\x46\x42\x36\xcc\xcd\x00\x00\x08\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x00\x00\x00\x00\x00\x00\x09\xc4'
+ msg += b'\x46\x00\x00\x00\x00\x00\x00\x0a\x28\x46\x00\x00\x00\x00\x00\x00\x0a\x8c\x46\x00\x00\x00\x00\x00\x00\x0a\xf0\x46\x00\x00\x00\x00\x00\x00\x0b\x54\x46\x3f\xd9\x99\x9a'
+ msg += b'\x00\x00\x0b\xb8\x46\x41\x8a\xe1\x48\x00\x00\x0c\x1c\x46\x3f\x8a\x3d\x71\x00\x00\x0c\x80\x46\x41\x1b\xd7\x0a\x00\x00\x0c\xe4\x46\x3f\x1e\xb8\x52\x00\x00\x0d\x48\x46'
+ msg += b'\x40\xf3\xd7\x0a\x00\x00\x0d\xac\x46\x00\x00\x00\x00\x00\x00\x0e\x10\x46\x00\x00\x00\x00\x00\x00\x0e\x74\x46\x00\x00\x00\x00\x00\x00\x0e\xd8\x46\x00\x00\x00\x00\x00'
+ msg += b'\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0\x53\x00\x00\x00\x00\x10\x04\x53\x55\xaa\x00\x00\x10\x68\x53\x00\x00\x00\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00'
+ msg += b'\x00\x00\x11\x94\x53\x00\x00\x00\x00\x11\xf8\x53\xff\xff\x00\x00\x12\x5c\x53\xff\xff\x00\x00\x12\xc0\x53\x00\x02\x00\x00\x13\x24\x53\xff\xff\x00\x00\x13\x88\x53\xff'
+ msg += b'\xff\x00\x00\x13\xec\x53\xff\xff\x00\x00\x14\x50\x53\xff\xff\x00\x00\x14\xb4\x53\xff\xff\x00\x00\x15\x18\x53\xff\xff\x00\x00\x15\x7c\x53\x00\x00\x00\x00\x27\x10\x53'
+ msg += b'\x00\x02\x00\x00\x27\x74\x53\x00\x3c\x00\x00\x27\xd8\x53\x00\x68\x00\x00\x28\x3c\x53\x05\x00\x00\x00\x28\xa0\x46\x43\x79\x00\x00\x00\x00\x29\x04\x46\x43\x48\x00\x00'
+ msg += b'\x00\x00\x29\x68\x46\x42\x48\x33\x33\x00\x00\x29\xcc\x46\x42\x3e\x3d\x71\x00\x00\x2a\x30\x53\x00\x01\x00\x00\x2a\x94\x46\x43\x37\x00\x00\x00\x00\x2a\xf8\x46\x42\xce'
+ msg += b'\x00\x00\x00\x00\x2b\x5c\x53\x00\x96\x00\x00\x2b\xc0\x53\x00\x10\x00\x00\x2c\x24\x46\x43\x90\x00\x00\x00\x00\x2c\x88\x46\x43\x95\x00\x00\x00\x00\x2c\xec\x53\x00\x06'
+ msg += b'\x00\x00\x2d\x50\x53\x00\x06\x00\x00\x2d\xb4\x46\x43\x7d\x00\x00\x00\x00\x2e\x18\x46\x42\x3d\xeb\x85\x00\x00\x2e\x7c\x46\x42\x3d\xeb\x85\x00\x00\x2e\xe0\x53\x00\x03'
+ msg += b'\x00\x00\x2f\x44\x53\x00\x03\x00\x00\x2f\xa8\x46\x42\x4d\xeb\x85\x00\x00\x30\x0c\x46\x42\x4d\xeb\x85\x00\x00\x30\x70\x53\x00\x03\x00\x00\x30\xd4\x53\x00\x03\x00\x00'
+ msg += b'\x31\x38\x46\x42\x08\x00\x00\x00\x00\x31\x9c\x53\x00\x05\x00\x00\x32\x00\x53\x04\x00\x00\x00\x32\x64\x53\x00\x01\x00\x00\x32\xc8\x53\x13\x9c\x00\x00\x33\x2c\x53\x0f'
+ msg += b'\xa0\x00\x00\x33\x90\x53\x00\x4f\x00\x00\x33\xf4\x53\x00\x66\x00\x00\x34\x58\x53\x03\xe8\x00\x00\x34\xbc\x53\x04\x00\x00\x00\x35\x20\x53\x00\x00\x00\x00\x35\x84\x53'
+ msg += b'\x00\x00\x00\x00\x35\xe8\x53\x00\x00\x00\x00\x36\x4c\x53\x00\x00\x00\x01\x38\x80\x53\x00\x02\x00\x01\x38\x81\x53\x00\x01\x00\x01\x38\x82\x53\x00\x01\x00\x01\x38\x83'
+ msg += b'\x53\x00\x00'
+ return msg
+
+@pytest.fixture
+def msg_inverter_ack(): # Get Time Request message
return b'\x00\x00\x00\x14\x10R170000000000001\x99\x04\x01'
@pytest.fixture
-def MsgInverterInvalid(): # Get Time Request message
+def msg_inverter_invalid(): # Get Time Request message
return b'\x00\x00\x00\x14\x10R170000000000001\x92\x04\x01'
@pytest.fixture
-def MsgUnknown(): # Get Time Request message
+def msg_unknown(): # Get Time Request message
return b'\x00\x00\x00\x17\x10R170000000000001\x91\x17\x01\x02\x03\x04'
@pytest.fixture
-def ConfigTsunAllowAll():
+def config_tsun_allow_all():
Config.config = {'tsun':{'enabled': True}, 'inverters':{'allow_all':True}}
@pytest.fixture
-def ConfigNoTsunInv1():
- Config.config = {'tsun':{'enabled': False},'inverters':{'R170000000000001':{'node_id':'inv1','suggested_area':'roof'}}}
+def config_no_tsun_inv1():
+ Config.config = {'tsun':{'enabled': False},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}}
@pytest.fixture
-def ConfigTsunInv1():
- Config.config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1','suggested_area':'roof'}}}
+def config_tsun_inv1():
+ Config.config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}}
@pytest.fixture
-def MsgOtaReq(): # Over the air update rewuest from tsun cloud
+def config_no_modbus_poll():
+ Config.config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': False, 'suggested_area':'roof'}}}
+
+@pytest.fixture
+def msg_ota_req(): # Over the air update request from tsun cloud
msg = b'\x00\x00\x01\x16\x10R170000000000001\x70\x13\x01\x02\x76\x35\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'\x6e\x69\x74\x6f\x72\x69\x6e\x67\x2e\x63\x6f\x6d\x3a\x39\x30\x30'
@@ -295,43 +388,43 @@ def MsgOtaReq(): # Over the air update rewuest from tsun cloud
return msg
@pytest.fixture
-def MsgOtaAck(): # Over the air update rewuest from tsun cloud
+def msg_ota_ack(): # Over the air update rewuest from tsun cloud
return b'\x00\x00\x00\x14\x10R170000000000001\x91\x13\x01'
@pytest.fixture
-def MsgOtaInvalid(): # Get Time Request message
+def msg_ota_invalid(): # Get Time Request message
return b'\x00\x00\x00\x14\x10R170000000000001\x99\x13\x01'
@pytest.fixture
-def MsgModbusCmd():
+def msg_modbus_cmd():
msg = b'\x00\x00\x00\x20\x10R170000000000001'
msg += b'\x70\x77\x00\x01\xa3\x28\x08\x01\x06\x20\x08'
msg += b'\x00\x00\x03\xc8'
return msg
@pytest.fixture
-def MsgModbusCmdCrcErr():
+def msg_modbus_cmd_crc_err():
msg = b'\x00\x00\x00\x20\x10R170000000000001'
msg += b'\x70\x77\x00\x01\xa3\x28\x08\x01\x06\x20\x08'
msg += b'\x00\x00\x04\xc8'
return msg
@pytest.fixture
-def MsgModbusRsp():
+def msg_modbus_rsp():
msg = b'\x00\x00\x00\x20\x10R170000000000001'
msg += b'\x91\x77\x17\x18\x19\x1a\x08\x01\x06\x20\x08'
msg += b'\x00\x00\x03\xc8'
return msg
@pytest.fixture
-def MsgModbusInv():
+def msg_modbus_inv():
msg = b'\x00\x00\x00\x20\x10R170000000000001'
msg += b'\x99\x77\x17\x18\x19\x1a\x08\x01\x06\x20\x08'
msg += b'\x00\x00\x03\xc8'
return msg
@pytest.fixture
-def MsgModbusResp20():
+def msg_modbus_rsp20():
msg = b'\x00\x00\x00\x45\x10R170000000000001'
msg += b'\x91\x77\x17\x18\x19\x1a\x2d\x01\x03\x28\x51'
msg += b'\x09\x08\xd3\x00\x29\x13\x87\x00\x3e\x00\x00\x01\x2c\x03\xb4\x00'
@@ -339,8 +432,101 @@ def MsgModbusResp20():
msg += b'\x00\x00\x00\x00\x00\x00\x00\xdb\x6b'
return msg
-def test_read_message(MsgContactInfo):
- m = MemoryStream(MsgContactInfo, (0,))
+@pytest.fixture
+def msg_modbus_rsp21():
+ msg = b'\x00\x00\x00\x45\x10R170000000000001'
+ msg += b'\x91\x77\x17\x18\x19\x1a\x2d\x01\x03\x28\x51'
+ msg += b'\x0e\x08\xd3\x00\x29\x13\x87\x00\x3e\x00\x00\x01\x2c\x03\xb4\x00'
+ msg += b'\x08\x00\x00\x00\x00\x01\x59\x01\x21\x03\xe6\x00\x00\x00\x00\x00'
+ msg += b'\x00\x00\x00\x00\x00\x00\x00\xe6\xef'
+ return msg
+
+@pytest.fixture
+def broken_recv_buf(): # There are two message in the buffer, but the second has overwritten the first partly
+ msg = b'\x00\x00\x05\x02\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001'
+ msg += b'\x01\x00\x00\x01\x89\xc6\x63\x61\x08'
+ msg += b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01'
+ msg += b'\x00\x00\x00\xc8\x53\x00\x00\x00\x00\x01\x2c\x53\x00\x02\x00\x00'
+ msg += b'\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00\x00\x00'
+ msg += b'\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94'
+ msg += b'\x53\x00\x00\x00\x00\x00\x05\x02\x10\x52\x31\x37\x45\x37\x33\x30'
+ msg += b'\x37\x30\x32\x31\x44\x30\x30\x36\x41\x91\x04\x01\x90\x00\x01\x10'
+ msg += b'\x54\x31\x37\x45\x37\x33\x30\x37\x30\x32\x31\x44\x30\x30\x36\x41'
+ msg += b'\x01\x00\x00\x01\x91\x1c\xe6\x80\xd0\x00\x00\x00\xa3\x00\x00\x00'
+ msg += b'\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x00\x00\x00\x01\x2c\x53'
+ msg += b'\x00\x02\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53'
+ msg += b'\x00\x00\x00\x00\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00'
+ msg += b'\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00'
+ msg += b'\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00\x00\x00\x00\x01\x98'
+ msg += b'\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00'
+ msg += b'\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00'
+ msg += b'\x00\x01\x9d\x53\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01'
+ msg += b'\x9f\x53\x00\x00\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49'
+ msg += b'\x00\x00\x00\x00\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf6\x53'
+ msg += b'\x00\x00\x00\x00\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00'
+ msg += b'\x00\x00\x01\xf9\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00'
+ msg += b'\x01\xfb\x53\x00\x00\x00\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd'
+ msg += b'\x53\x00\x00\x00\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00'
+ msg += b'\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00\x00'
+ msg += b'\x00\x02\x02\x53\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02'
+ msg += b'\x04\x53\x00\x00\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02'
+ msg += b'\x59\x53\x00\x00\x00\x00\x02\x5a\x53\x00\x00\x00\x00\x02\x5b\x53'
+ msg += b'\x00\x00\x00\x00\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00'
+ msg += b'\x00\x00\x02\x5e\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00'
+ msg += b'\x02\x60\x53\x00\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62'
+ msg += b'\x53\x00\x00\x00\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00'
+ msg += b'\x00\x00\x00\x02\x65\x53\x00\x00\x00\x00\x02\x66\x53\x00\x00\x00'
+ msg += b'\x00\x02\x67\x53\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02'
+ msg += b'\xbc\x49\x00\x00\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02'
+ msg += b'\xbe\x53\x00\x00\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53'
+ msg += b'\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00'
+ msg += b'\x00\x00\x02\xc3\x53\x00\x00\x00\x00\x02\xc4\x53\x00\x00\x00\x00'
+ msg += b'\x02\xc5\x53\x00\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7'
+ msg += b'\x53\x00\x00\x00\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00'
+ msg += b'\x00\x00\x00\x02\xca\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00'
+ msg += b'\x00\x02\xcc\x53\x00\x00\x00\x00\x03\x20\x53\x00\x00\x00\x00\x03'
+ msg += b'\x84\x53\x51\x09\x00\x00\x03\xe8\x46\x43\x62\xb3\x33\x00\x00\x04'
+ msg += b'\x4c\x46\x3e\xc2\x8f\x5c\x00\x00\x04\xb0\x46\x42\x48\x00\x00\x00'
+ msg += b'\x00\x05\x14\x53\x00\x18\x00\x00\x05\x78\x53\x00\x00\x00\x00\x05'
+ msg += b'\xdc\x53\x02\x58\x00\x00\x06\x40\x46\x42\xae\xcc\xcd\x00\x00\x06'
+ msg += b'\xa4\x46\x3f\x4c\xcc\xcd\x00\x00\x07\x08\x46\x00\x00\x00\x00\x00'
+ msg += b'\x00\x07\x6c\x46\x00\x00\x00\x00\x00\x00\x07\xd0\x46\x42\x0a\x66'
+ msg += b'\x66\x00\x00\x08\x34\x46\x40\x2a\x3d\x71\x00\x00\x08\x98\x46\x42'
+ msg += b'\xb8\x33\x33\x00\x00\x08\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60'
+ msg += b'\x46\x00\x00\x00\x00\x00\x00\x09\xc4\x46\x00\x00\x00\x00\x00\x00'
+ msg += b'\x0a\x28\x46\x00\x00\x00\x00\x00\x00\x0a\x8c\x46\x00\x00\x00\x00'
+ msg += b'\x00\x00\x0a\xf0\x46\x00\x00\x00\x00\x00\x00\x0b\x54\x46\x3e\x05'
+ msg += b'\x1e\xb8\x00\x00\x0b\xb8\x46\x43\xe2\x42\x8f\x00\x00\x0c\x1c\x46'
+ msg += b'\x00\x00\x00\x00\x00\x00\x0c\x80\x46\x43\x04\x4a\x3d\x00\x00\x0c'
+ msg += b'\xe4\x46\x3e\x0f\x5c\x29\x00\x00\x0d\x48\x46\x43\xad\x48\xf6\x00'
+ msg += b'\x00\x0d\xac\x46\x00\x00\x00\x00\x00\x00\x0e\x10\x46\x00\x00\x00'
+ msg += b'\x00\x00\x00\x0e\x74\x46\x00\x00\x00\x00\x00\x00\x0e\xd8\x46\x00'
+ msg += b'\x00\x00\x00\x00\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0\x53\x00'
+ msg += b'\x00\x00\x00\x10\x04\x53\x55\xaa\x00\x00\x10\x68\x53\x00\x01\x00'
+ msg += b'\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00\x00\x00\x11'
+ msg += b'\x94\x53\x00\x00\x00\x00\x11\xf8\x53\xff\xff\x00\x00\x12\x5c\x53'
+ msg += b'\x03\x20\x00\x00\x12\xc0\x53\x00\x02\x00\x00\x13\x24\x53\x04\x00'
+ msg += b'\x00\x00\x13\x88\x53\x04\x00\x00\x00\x13\xec\x53\x04\x00\x00\x00'
+ msg += b'\x14\x50\x53\x04\x00\x00\x00\x14\xb4\x53\x00\x01\x00\x00\x15\x18'
+ msg += b'\x53\x08\x04\x00\x00\x15\x7c\x53\x00\x00\x00\x00\x27\x10\x53\x00'
+ msg += b'\x02\x00\x00\x27\x74\x53\x00\x3c\x00\x00\x27\xd8\x53\x00\x68\x00'
+ msg += b'\x00\x28\x3c\x53\x05\x00\x00\x00\x28\xa0\x46\x43\x79\x00\x00\x00'
+ msg += b'\x00\x29\x04\x46\x43\x48\x00\x00\x00\x00\x29\x68\x46\x42\x48\x33'
+ msg += b'\x33\x00\x00\x29\xcc\x46\x42\x3e\x3d\x71\x00\x00\x2a\x30\x53\x00'
+ msg += b'\x01\x00\x00\x2a\x94\x46\x43\x37\x00\x00\x00\x00\x2a\xf8\x46\x42'
+ msg += b'\xce\x00\x00\x00\x00\x2b\x5c\x53\x00\x96\x00\x00\x2b\xc0\x53\x00'
+ msg += b'\x10\x00\x00\x2c\x24\x46\x43\x90\x00\x00\x00\x00\x2c\x88\x46\x43'
+ msg += b'\x95\x00\x00\x00\x00\x2c\xec\x53\x00\x06\x00\x00\x2d\x50\x53\x00'
+ msg += b'\x06\x00\x00\x2d\xb4\x46\x43\x7d\x00\x00\x00\x00\x2e\x18\x46\x42'
+ msg += b'\x3d\xeb\x85\x00\x00\x2e\x7c\x46\x42\x3d\xeb\x85\x00\x00\x2e\xe0'
+ msg += b'\x53\x00\x03\x00\x00\x2f\x44\x53\x00\x03\x00\x00\x2f\xa8\x46\x42'
+ msg += b'\x4d\xeb\x85\x00\x00\x30\x0c\x46\x42\x4d\xeb\x85\x00\x00\x30\x70'
+ msg += b'\x53\x00\x03\x00\x00\x30\xd4\x53\x00\x03\x00\x00\x31\x38\x46\x42'
+ msg += b'\x08\x00\x00\x00\x00\x31'
+ return msg
+
+def test_read_message(msg_contact_info):
+ m = MemoryStream(msg_contact_info, (0,))
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 1
@@ -353,34 +539,28 @@ def test_read_message(MsgContactInfo):
assert m._forward_buffer==b''
m.close()
-def test_read_message_twice(ConfigNoTsunInv1, MsgInverterInd):
- ConfigNoTsunInv1
- m = MemoryStream(MsgInverterInd, (0,))
- m.append_msg(MsgInverterInd)
- m.read() # read complete msg, and dispatch msg
- assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
- assert m.msg_count == 1
- assert m.id_str == b"R170000000000001"
- assert m.unique_id == 'R170000000000001'
- assert int(m.ctrl)==145
- assert m.msg_id==4
- assert m.header_len==23
- assert m.data_len==120
- assert m._forward_buffer==b''
+def test_read_message_twice(config_no_tsun_inv1, msg_inverter_ind):
+ config_no_tsun_inv1
+ m = MemoryStream(msg_inverter_ind, (0,))
+ m.append_msg(msg_inverter_ind)
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 2
+ assert m.msg_recvd[0]['ctrl']==145
+ assert m.msg_recvd[0]['msg_id']==4
+ assert m.msg_recvd[0]['header_len']==23
+ assert m.msg_recvd[0]['data_len']==120
+ assert m.msg_recvd[1]['ctrl']==145
+ assert m.msg_recvd[1]['msg_id']==4
+ assert m.msg_recvd[1]['header_len']==23
+ assert m.msg_recvd[1]['data_len']==120
assert m.id_str == b"R170000000000001"
assert m.unique_id == 'R170000000000001'
- assert int(m.ctrl)==145
- assert m.msg_id==4
- assert m.header_len==23
- assert m.data_len==120
assert m._forward_buffer==b''
m.close()
-def test_read_message_long_id(MsgContactInfo_LongId):
- m = MemoryStream(MsgContactInfo_LongId, (23,24))
+def test_read_message_long_id(msg_contact_info_long_id):
+ m = MemoryStream(msg_contact_info_long_id, (23,24))
m.read() # read 23 bytes, one is missing
assert not m.header_valid # must be invalid, since header not complete
assert m.msg_count == 0
@@ -399,8 +579,8 @@ def test_read_message_long_id(MsgContactInfo_LongId):
m.close()
-def test_read_message_in_chunks(MsgContactInfo):
- m = MemoryStream(MsgContactInfo, (4,23,0))
+def test_read_message_in_chunks(msg_contact_info):
+ m = MemoryStream(msg_contact_info, (4,23,0))
m.read() # read 4 bytes, header incomplere
assert not m.header_valid # must be invalid, since header not complete
assert m.msg_count == 0
@@ -418,8 +598,8 @@ def test_read_message_in_chunks(MsgContactInfo):
assert m.msg_count == 1
m.close()
-def test_read_message_in_chunks2(MsgContactInfo):
- m = MemoryStream(MsgContactInfo, (4,10,0))
+def test_read_message_in_chunks2(msg_contact_info):
+ m = MemoryStream(msg_contact_info, (4,10,0))
m.read() # read 4 bytes, header incomplere
assert not m.header_valid
assert m.msg_count == 0
@@ -440,41 +620,27 @@ def test_read_message_in_chunks2(MsgContactInfo):
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
m.close()
-def test_read_two_messages(ConfigTsunAllowAll, Msg2ContactInfo,MsgContactResp,MsgContactResp2):
- ConfigTsunAllowAll
- m = MemoryStream(Msg2ContactInfo, (0,))
+def test_read_two_messages(config_tsun_allow_all, msg2_contact_info,msg_contact_rsp,msg_contact_rsp2):
+ config_tsun_allow_all
+ m = MemoryStream(msg2_contact_info, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
- assert m.msg_count == 1
- assert m.id_str == b"R170000000000001"
- assert m.unique_id == 'R170000000000001'
- assert int(m.ctrl)==145
- assert m.msg_id==0
- assert m.header_len==23
- assert m.data_len==25
- assert m._forward_buffer==b''
- assert m._send_buffer==MsgContactResp
- assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
-
- m._send_buffer = bytearray(0) # clear send buffer for next test
- m.contact_name = b'solarhub'
- m.contact_mail = b'solarhub@123456'
- m._init_new_client_conn()
- assert m._send_buffer==b'\x00\x00\x00,\x10R170000000000001\x91\x00\x08solarhub\x0fsolarhub@123456'
-
- m._send_buffer = bytearray(0) # clear send buffer for next test
- m.read() # read complete msg, and dispatch msg
- assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.msg_count == 2
assert m.id_str == b"R170000000000002"
assert m.unique_id == 'R170000000000002'
- assert int(m.ctrl)==145
- assert m.msg_id==0
- assert m.header_len==23
- assert m.data_len==25
+ m.contact_name = b'solarhub'
+ m.contact_mail = b'solarhub@123456'
+ assert m.msg_recvd[0]['ctrl']==145
+ assert m.msg_recvd[0]['msg_id']==0
+ assert m.msg_recvd[0]['header_len']==23
+ assert m.msg_recvd[0]['data_len']==25
+ assert m.msg_recvd[1]['ctrl']==145
+ assert m.msg_recvd[1]['msg_id']==0
+ assert m.msg_recvd[1]['header_len']==23
+ assert m.msg_recvd[1]['data_len']==25
assert m._forward_buffer==b''
- assert m._send_buffer==MsgContactResp2
+ assert m._send_buffer==msg_contact_rsp + msg_contact_rsp2
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m._send_buffer = bytearray(0) # clear send buffer for next test
@@ -484,9 +650,9 @@ def test_read_two_messages(ConfigTsunAllowAll, Msg2ContactInfo,MsgContactResp,Ms
assert m._send_buffer==b'\x00\x00\x00,\x10R170000000000002\x91\x00\x08solarhub\x0fsolarhub@123456'
m.close()
-def test_msg_contact_resp(ConfigTsunInv1, MsgContactResp):
- ConfigTsunInv1
- m = MemoryStream(MsgContactResp, (0,), False)
+def test_msg_contact_resp(config_tsun_inv1, msg_contact_rsp):
+ config_tsun_inv1
+ m = MemoryStream(msg_contact_rsp, (0,), False)
m.await_conn_resp_cnt = 1
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
@@ -504,9 +670,9 @@ def test_msg_contact_resp(ConfigTsunInv1, MsgContactResp):
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
-def test_msg_contact_resp_2(ConfigTsunInv1, MsgContactResp):
- ConfigTsunInv1
- m = MemoryStream(MsgContactResp, (0,), False)
+def test_msg_contact_resp_2(config_tsun_inv1, msg_contact_rsp):
+ config_tsun_inv1
+ m = MemoryStream(msg_contact_rsp, (0,), False)
m.await_conn_resp_cnt = 0
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
@@ -519,14 +685,14 @@ def test_msg_contact_resp_2(ConfigTsunInv1, MsgContactResp):
assert m.msg_id==0
assert m.header_len==23
assert m.data_len==1
- assert m._forward_buffer==MsgContactResp
+ assert m._forward_buffer==msg_contact_rsp
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
-def test_msg_contact_resp_3(ConfigTsunInv1, MsgContactResp):
- ConfigTsunInv1
- m = MemoryStream(MsgContactResp, (0,), True)
+def test_msg_contact_resp_3(config_tsun_inv1, msg_contact_rsp):
+ config_tsun_inv1
+ m = MemoryStream(msg_contact_rsp, (0,), True)
m.await_conn_resp_cnt = 0
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
@@ -539,14 +705,14 @@ def test_msg_contact_resp_3(ConfigTsunInv1, MsgContactResp):
assert m.msg_id==0
assert m.header_len==23
assert m.data_len==1
- assert m._forward_buffer==MsgContactResp
+ assert m._forward_buffer==msg_contact_rsp
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
-def test_msg_contact_invalid(ConfigTsunInv1, MsgContactInvalid):
- ConfigTsunInv1
- m = MemoryStream(MsgContactInvalid, (0,))
+def test_msg_contact_invalid(config_tsun_inv1, msg_contact_invalid):
+ config_tsun_inv1
+ m = MemoryStream(msg_contact_invalid, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -557,14 +723,14 @@ def test_msg_contact_invalid(ConfigTsunInv1, MsgContactInvalid):
assert m.msg_id==0
assert m.header_len==23
assert m.data_len==1
- assert m._forward_buffer==MsgContactInvalid
+ assert m._forward_buffer==msg_contact_invalid
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
m.close()
-def test_msg_get_time(ConfigTsunInv1, MsgGetTime):
- ConfigTsunInv1
- m = MemoryStream(MsgGetTime, (0,))
+def test_msg_get_time(config_tsun_inv1, msg_get_time):
+ config_tsun_inv1
+ m = MemoryStream(msg_get_time, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -576,14 +742,14 @@ def test_msg_get_time(ConfigTsunInv1, MsgGetTime):
assert m.header_len==23
assert m.ts_offset==0
assert m.data_len==0
- assert m._forward_buffer==MsgGetTime
+ assert m._forward_buffer==msg_get_time
assert m._send_buffer==b'\x00\x00\x00\x1b\x10R170000000000001\x91"\x00\x00\x01\x89\xc6,_\x00'
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
-def test_msg_get_time_autark(ConfigNoTsunInv1, MsgGetTime):
- ConfigNoTsunInv1
- m = MemoryStream(MsgGetTime, (0,))
+def test_msg_get_time_autark(config_no_tsun_inv1, msg_get_time):
+ config_no_tsun_inv1
+ m = MemoryStream(msg_get_time, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -600,9 +766,9 @@ def test_msg_get_time_autark(ConfigNoTsunInv1, MsgGetTime):
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
-def test_msg_time_resp(ConfigTsunInv1, MsgTimeResp):
- ConfigTsunInv1
- m = MemoryStream(MsgTimeResp, (0,), False)
+def test_msg_time_resp(config_tsun_inv1, msg_time_rsp):
+ config_tsun_inv1
+ m = MemoryStream(msg_time_rsp, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -619,9 +785,9 @@ def test_msg_time_resp(ConfigTsunInv1, MsgTimeResp):
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
-def test_msg_time_resp_autark(ConfigNoTsunInv1, MsgTimeResp):
- ConfigNoTsunInv1
- m = MemoryStream(MsgTimeResp, (0,), False)
+def test_msg_time_resp_autark(config_no_tsun_inv1, msg_time_rsp):
+ config_no_tsun_inv1
+ m = MemoryStream(msg_time_rsp, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -638,9 +804,9 @@ def test_msg_time_resp_autark(ConfigNoTsunInv1, MsgTimeResp):
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
-def test_msg_time_inv_resp(ConfigTsunInv1, MsgTimeRespInv):
- ConfigTsunInv1
- m = MemoryStream(MsgTimeRespInv, (0,), False)
+def test_msg_time_inv_resp(config_tsun_inv1, msg_time_rsp_inv):
+ config_tsun_inv1
+ m = MemoryStream(msg_time_rsp_inv, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -652,14 +818,14 @@ def test_msg_time_inv_resp(ConfigTsunInv1, MsgTimeRespInv):
assert m.header_len==23
assert m.ts_offset==0
assert m.data_len==4
- assert m._forward_buffer==MsgTimeRespInv
+ assert m._forward_buffer==msg_time_rsp_inv
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
-def test_msg_time_invalid(ConfigTsunInv1, MsgTimeInvalid):
- ConfigTsunInv1
- m = MemoryStream(MsgTimeInvalid, (0,), False)
+def test_msg_time_invalid(config_tsun_inv1, msg_time_invalid):
+ config_tsun_inv1
+ m = MemoryStream(msg_time_invalid, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -671,14 +837,14 @@ def test_msg_time_invalid(ConfigTsunInv1, MsgTimeInvalid):
assert m.header_len==23
assert m.ts_offset==0
assert m.data_len==0
- assert m._forward_buffer==MsgTimeInvalid
+ assert m._forward_buffer==msg_time_invalid
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
m.close()
-def test_msg_time_invalid_autark(ConfigNoTsunInv1, MsgTimeInvalid):
- ConfigNoTsunInv1
- m = MemoryStream(MsgTimeInvalid, (0,), False)
+def test_msg_time_invalid_autark(config_no_tsun_inv1, msg_time_invalid):
+ config_no_tsun_inv1
+ m = MemoryStream(msg_time_invalid, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -695,9 +861,9 @@ def test_msg_time_invalid_autark(ConfigNoTsunInv1, MsgTimeInvalid):
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
m.close()
-def test_msg_cntrl_ind(ConfigTsunInv1, MsgControllerInd, MsgControllerIndTsOffs, MsgControllerAck):
- ConfigTsunInv1
- m = MemoryStream(MsgControllerInd, (0,))
+def test_msg_cntrl_ind(config_tsun_inv1, msg_controller_ind, msg_controller_ind_ts_offs, msg_controller_ack):
+ config_tsun_inv1
+ m = MemoryStream(msg_controller_ind, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -710,17 +876,17 @@ def test_msg_cntrl_ind(ConfigTsunInv1, MsgControllerInd, MsgControllerIndTsOffs,
assert m.data_len==284
m.ts_offset = 0
m._update_header(m._forward_buffer)
- assert m._forward_buffer==MsgControllerInd
+ assert m._forward_buffer==msg_controller_ind
m.ts_offset = -4096
m._update_header(m._forward_buffer)
- assert m._forward_buffer==MsgControllerIndTsOffs
- assert m._send_buffer==MsgControllerAck
+ assert m._forward_buffer==msg_controller_ind_ts_offs
+ assert m._send_buffer==msg_controller_ack
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
-def test_msg_cntrl_ack(ConfigTsunInv1, MsgControllerAck):
- ConfigTsunInv1
- m = MemoryStream(MsgControllerAck, (0,), False)
+def test_msg_cntrl_ack(config_tsun_inv1, msg_controller_ack):
+ config_tsun_inv1
+ m = MemoryStream(msg_controller_ack, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -736,9 +902,9 @@ def test_msg_cntrl_ack(ConfigTsunInv1, MsgControllerAck):
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
-def test_msg_cntrl_invalid(ConfigTsunInv1, MsgControllerInvalid):
- ConfigTsunInv1
- m = MemoryStream(MsgControllerInvalid, (0,))
+def test_msg_cntrl_invalid(config_tsun_inv1, msg_controller_invalid):
+ config_tsun_inv1
+ m = MemoryStream(msg_controller_invalid, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -751,18 +917,18 @@ def test_msg_cntrl_invalid(ConfigTsunInv1, MsgControllerInvalid):
assert m.data_len==1
m.ts_offset = 0
m._update_header(m._forward_buffer)
- assert m._forward_buffer==MsgControllerInvalid
+ assert m._forward_buffer==msg_controller_invalid
m.ts_offset = -4096
m._update_header(m._forward_buffer)
- assert m._forward_buffer==MsgControllerInvalid
+ assert m._forward_buffer==msg_controller_invalid
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
m.close()
-def test_msg_inv_ind(ConfigTsunInv1, MsgInverterInd, MsgInverterIndTsOffs, MsgInverterAck):
- ConfigTsunInv1
+def test_msg_inv_ind(config_tsun_inv1, msg_inverter_ind, msg_inverter_ind_ts_offs, msg_inverter_ack):
+ config_tsun_inv1
tracer.setLevel(logging.DEBUG)
- m = MemoryStream(MsgInverterInd, (0,))
+ m = MemoryStream(msg_inverter_ind, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -775,18 +941,42 @@ def test_msg_inv_ind(ConfigTsunInv1, MsgInverterInd, MsgInverterIndTsOffs, MsgIn
assert m.data_len==120
m.ts_offset = 0
m._update_header(m._forward_buffer)
- assert m._forward_buffer==MsgInverterInd
+ assert m._forward_buffer==msg_inverter_ind
m.ts_offset = +256
m._update_header(m._forward_buffer)
- assert m._forward_buffer==MsgInverterIndTsOffs
- assert m._send_buffer==MsgInverterAck
+ assert m._forward_buffer==msg_inverter_ind_ts_offs
+ assert m._send_buffer==msg_inverter_ack
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
-def test_msg_inv_ind2(ConfigTsunInv1, MsgInverterIndNew, MsgInverterIndTsOffs, MsgInverterAck):
- ConfigTsunInv1
+def test_msg_inv_ind1(config_tsun_inv1, msg_inverter_ind2, msg_inverter_ind_ts_offs, msg_inverter_ack):
+ config_tsun_inv1
tracer.setLevel(logging.DEBUG)
- m = MemoryStream(MsgInverterIndNew, (0,))
+ m = MemoryStream(msg_inverter_ind2, (0,))
+ m.db.stat['proxy']['Unknown_Ctrl'] = 0
+ m.db.stat['proxy']['Invalid_Data_Type'] = 0
+ m.read() # read complete msg, and dispatch msg
+ assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
+ assert m.db.stat['proxy']['Invalid_Data_Type'] == 0
+ assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
+ assert m.msg_count == 1
+ assert m.id_str == b"R170000000000001"
+ assert m.unique_id == 'R170000000000001'
+ assert int(m.ctrl)==145
+ assert m.msg_id==4
+ assert m.header_len==23
+ assert m.data_len==1263
+ m.ts_offset = 0
+ m._update_header(m._forward_buffer)
+ assert m._forward_buffer==msg_inverter_ind2
+ assert m._send_buffer==msg_inverter_ack
+ assert m.db.get_db_value(Register.TS_GRID) == 1691243349
+ m.close()
+
+def test_msg_inv_ind2(config_tsun_inv1, msg_inverter_ind_new, msg_inverter_ind_ts_offs, msg_inverter_ack):
+ config_tsun_inv1
+ tracer.setLevel(logging.DEBUG)
+ m = MemoryStream(msg_inverter_ind_new, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Invalid_Data_Type'] = 0
m.read() # read complete msg, and dispatch msg
@@ -802,14 +992,19 @@ def test_msg_inv_ind2(ConfigTsunInv1, MsgInverterIndNew, MsgInverterIndTsOffs, M
assert m.data_len==1165
m.ts_offset = 0
m._update_header(m._forward_buffer)
- assert m._forward_buffer==MsgInverterIndNew
- assert m._send_buffer==MsgInverterAck
+ assert m._forward_buffer==msg_inverter_ind_new
+ assert m._send_buffer==msg_inverter_ack
+ assert m.db.get_db_value(Register.INVERTER_STATUS) == None
+ assert m.db.get_db_value(Register.TS_GRID) == None
+ m.db.db['grid'] = {'Output_Power': 100}
m.close()
+ assert m.db.get_db_value(Register.INVERTER_STATUS) == None
-def test_msg_inv_ind2(ConfigTsunInv1, MsgInverterIndNew, MsgInverterIndTsOffs, MsgInverterAck):
- ConfigTsunInv1
+def test_msg_inv_ind3(config_tsun_inv1, msg_inverter_ind_0w, msg_inverter_ack):
+ '''test that after close the invert_status will be resetted if the grid power is <2W'''
+ config_tsun_inv1
tracer.setLevel(logging.DEBUG)
- m = MemoryStream(MsgInverterIndNew, (0,))
+ m = MemoryStream(msg_inverter_ind_0w, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Invalid_Data_Type'] = 0
m.read() # read complete msg, and dispatch msg
@@ -822,18 +1017,22 @@ def test_msg_inv_ind2(ConfigTsunInv1, MsgInverterIndNew, MsgInverterIndTsOffs, M
assert int(m.ctrl)==145
assert m.msg_id==4
assert m.header_len==23
- assert m.data_len==1165
+ assert m.data_len==1263
m.ts_offset = 0
m._update_header(m._forward_buffer)
- assert m._forward_buffer==MsgInverterIndNew
- assert m._send_buffer==MsgInverterAck
+ assert m._forward_buffer==msg_inverter_ind_0w
+ assert m._send_buffer==msg_inverter_ack
+ assert m.db.get_db_value(Register.INVERTER_STATUS) == None
+ assert m.db.db['grid']['Output_Power'] == 0.5
m.close()
+ assert m.db.get_db_value(Register.INVERTER_STATUS) == 0
-def test_msg_inv_ack(ConfigTsunInv1, MsgInverterAck):
- ConfigTsunInv1
+
+def test_msg_inv_ack(config_tsun_inv1, msg_inverter_ack):
+ config_tsun_inv1
tracer.setLevel(logging.ERROR)
- m = MemoryStream(MsgInverterAck, (0,), False)
+ m = MemoryStream(msg_inverter_ack, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -849,9 +1048,9 @@ def test_msg_inv_ack(ConfigTsunInv1, MsgInverterAck):
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m.close()
-def test_msg_inv_invalid(ConfigTsunInv1, MsgInverterInvalid):
- ConfigTsunInv1
- m = MemoryStream(MsgInverterInvalid, (0,), False)
+def test_msg_inv_invalid(config_tsun_inv1, msg_inverter_invalid):
+ config_tsun_inv1
+ m = MemoryStream(msg_inverter_invalid, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -864,17 +1063,17 @@ def test_msg_inv_invalid(ConfigTsunInv1, MsgInverterInvalid):
assert m.data_len==1
m.ts_offset = 0
m._update_header(m._forward_buffer)
- assert m._forward_buffer==MsgInverterInvalid
+ assert m._forward_buffer==msg_inverter_invalid
m.ts_offset = 256
m._update_header(m._forward_buffer)
- assert m._forward_buffer==MsgInverterInvalid
+ assert m._forward_buffer==msg_inverter_invalid
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
m.close()
-def test_msg_ota_req(ConfigTsunInv1, MsgOtaReq):
- ConfigTsunInv1
- m = MemoryStream(MsgOtaReq, (0,), False)
+def test_msg_ota_req(config_tsun_inv1, msg_ota_req):
+ config_tsun_inv1
+ m = MemoryStream(msg_ota_req, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['OTA_Start_Msg'] = 0
m.read() # read complete msg, and dispatch msg
@@ -888,20 +1087,20 @@ def test_msg_ota_req(ConfigTsunInv1, MsgOtaReq):
assert m.data_len==259
m.ts_offset = 0
m._update_header(m._forward_buffer)
- assert m._forward_buffer==MsgOtaReq
+ assert m._forward_buffer==msg_ota_req
m.ts_offset = 4096
m._update_header(m._forward_buffer)
- assert m._forward_buffer==MsgOtaReq
+ assert m._forward_buffer==msg_ota_req
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['OTA_Start_Msg'] == 1
m.close()
-def test_msg_ota_ack(ConfigTsunInv1, MsgOtaAck):
- ConfigTsunInv1
+def test_msg_ota_ack(config_tsun_inv1, msg_ota_ack):
+ config_tsun_inv1
tracer.setLevel(logging.ERROR)
- m = MemoryStream(MsgOtaAck, (0,), False)
+ m = MemoryStream(msg_ota_ack, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['OTA_Start_Msg'] = 0
m.read() # read complete msg, and dispatch msg
@@ -915,18 +1114,18 @@ def test_msg_ota_ack(ConfigTsunInv1, MsgOtaAck):
assert m.data_len==1
m.ts_offset = 0
m._update_header(m._forward_buffer)
- assert m._forward_buffer==MsgOtaAck
+ assert m._forward_buffer==msg_ota_ack
m.ts_offset = 256
m._update_header(m._forward_buffer)
- assert m._forward_buffer==MsgOtaAck
+ assert m._forward_buffer==msg_ota_ack
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['OTA_Start_Msg'] == 0
m.close()
-def test_msg_ota_invalid(ConfigTsunInv1, MsgOtaInvalid):
- ConfigTsunInv1
- m = MemoryStream(MsgOtaInvalid, (0,), False)
+def test_msg_ota_invalid(config_tsun_inv1, msg_ota_invalid):
+ config_tsun_inv1
+ m = MemoryStream(msg_ota_invalid, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['OTA_Start_Msg'] = 0
m.read() # read complete msg, and dispatch msg
@@ -940,18 +1139,18 @@ def test_msg_ota_invalid(ConfigTsunInv1, MsgOtaInvalid):
assert m.data_len==1
m.ts_offset = 0
m._update_header(m._forward_buffer)
- assert m._forward_buffer==MsgOtaInvalid
+ assert m._forward_buffer==msg_ota_invalid
m.ts_offset = 4096
- assert m._forward_buffer==MsgOtaInvalid
+ assert m._forward_buffer==msg_ota_invalid
m._update_header(m._forward_buffer)
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
assert m.db.stat['proxy']['OTA_Start_Msg'] == 0
m.close()
-def test_msg_unknown(ConfigTsunInv1, MsgUnknown):
- ConfigTsunInv1
- m = MemoryStream(MsgUnknown, (0,), False)
+def test_msg_unknown(config_tsun_inv1, msg_unknown):
+ config_tsun_inv1
+ m = MemoryStream(msg_unknown, (0,), False)
m.db.stat['proxy']['Unknown_Msg'] = 0
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -962,7 +1161,7 @@ def test_msg_unknown(ConfigTsunInv1, MsgUnknown):
assert m.msg_id==23
assert m.header_len==23
assert m.data_len==4
- assert m._forward_buffer==MsgUnknown
+ assert m._forward_buffer==msg_unknown
assert m._send_buffer==b''
assert 1 == m.db.stat['proxy']['Unknown_Msg']
m.close()
@@ -1002,9 +1201,24 @@ def test_msg_iterator():
assert test1 == 1
assert test2 == 1
+def test_timestamp_cnv():
+ '''test converting inverter timestamps into utc'''
+ m = MemoryStream(b'')
+ ts = 1722645998453 # Saturday, 3. August 2024 00:46:38.453 (GMT+2:00)
+ utc =1722638798.453 # GMT: Friday, 2. August 2024 22:46:38.453
+ assert utc == m._utcfromts(ts)
+
+ ts = 1691246944000 # Saturday, 5. August 2023 14:49:04 (GMT+2:00)
+ utc =1691239744.0 # GMT: Saturday, 5. August 2023 12:49:04
+ assert utc == m._utcfromts(ts)
+
+ ts = 1704152544000 # Monday, 1. January 2024 23:42:24 (GMT+1:00)
+ utc =1704148944.0 # GMT: Monday, 1. January 2024 22:42:24
+ assert utc == m._utcfromts(ts)
+
+ m.close()
+
def test_proxy_counter():
- # m = MemoryStream(b'')
- # m.close()
Infos.stat['proxy']['Modbus_Command'] = 1
m = MemoryStream(b'')
@@ -1052,13 +1266,13 @@ def test_proxy_counter():
assert 1 == m.db.stat['proxy']['Unknown_Msg']
m.close()
-def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd):
- ConfigTsunInv1
+def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd):
+ config_tsun_inv1
m = MemoryStream(b'')
m.id_str = b"R170000000000001"
m.state = State.up
- c = m.createClientStream(MsgModbusCmd)
+ c = m.createClientStream(msg_modbus_cmd)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
@@ -1077,18 +1291,18 @@ def test_msg_modbus_req(ConfigTsunInv1, MsgModbusCmd):
assert m.id_str == b"R170000000000001"
assert m._forward_buffer==b''
assert m._send_buffer==b''
- assert m.writer.sent_pdu == MsgModbusCmd
+ assert m.writer.sent_pdu == msg_modbus_cmd
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
assert m.db.stat['proxy']['Modbus_Command'] == 1
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_msg_modbus_req2(ConfigTsunInv1, MsgModbusCmd):
- ConfigTsunInv1
+def test_msg_modbus_req2(config_tsun_inv1, msg_modbus_cmd):
+ config_tsun_inv1
m = MemoryStream(b'')
m.id_str = b"R170000000000001"
- c = m.createClientStream(MsgModbusCmd)
+ c = m.createClientStream(msg_modbus_cmd)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
@@ -1113,11 +1327,11 @@ def test_msg_modbus_req2(ConfigTsunInv1, MsgModbusCmd):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
m.close()
-def test_msg_modbus_req3(ConfigTsunInv1, MsgModbusCmdCrcErr):
- ConfigTsunInv1
+def test_msg_modbus_req3(config_tsun_inv1, msg_modbus_cmd_crc_err):
+ config_tsun_inv1
m = MemoryStream(b'')
m.id_str = b"R170000000000001"
- c = m.createClientStream(MsgModbusCmdCrcErr)
+ c = m.createClientStream(msg_modbus_cmd_crc_err)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
@@ -1141,10 +1355,10 @@ def test_msg_modbus_req3(ConfigTsunInv1, MsgModbusCmdCrcErr):
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1
m.close()
-def test_msg_modbus_rsp1(ConfigTsunInv1, MsgModbusRsp):
+def test_msg_modbus_rsp1(config_tsun_inv1, msg_modbus_rsp):
'''Modbus response without a valid Modbus request must be dropped'''
- ConfigTsunInv1
- m = MemoryStream(MsgModbusRsp)
+ config_tsun_inv1
+ m = MemoryStream(msg_modbus_rsp)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
m.read() # read complete msg, and dispatch msg
@@ -1162,10 +1376,10 @@ def test_msg_modbus_rsp1(ConfigTsunInv1, MsgModbusRsp):
assert m.db.stat['proxy']['Modbus_Command'] == 0
m.close()
-def test_msg_modbus_cloud_rsp(ConfigTsunInv1, MsgModbusRsp):
+def test_msg_modbus_cloud_rsp(config_tsun_inv1, msg_modbus_rsp):
'''Modbus response from TSUN without a valid Modbus request must be dropped'''
- ConfigTsunInv1
- m = MemoryStream(MsgModbusRsp, (0,), False)
+ config_tsun_inv1
+ m = MemoryStream(msg_modbus_rsp, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Unknown_Msg'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
@@ -1185,11 +1399,11 @@ def test_msg_modbus_cloud_rsp(ConfigTsunInv1, MsgModbusRsp):
assert m.db.stat['proxy']['Modbus_Command'] == 0
m.close()
-def test_msg_modbus_rsp2(ConfigTsunInv1, MsgModbusResp20):
+def test_msg_modbus_rsp2(config_tsun_inv1, msg_modbus_rsp20):
'''Modbus response with a valid Modbus request must be forwarded'''
- ConfigTsunInv1
- m = MemoryStream(MsgModbusResp20)
- m.append_msg(MsgModbusResp20)
+ config_tsun_inv1
+ m = MemoryStream(msg_modbus_rsp20)
+ m.append_msg(msg_modbus_rsp20)
m.mb.rsp_handler = m.msg_forward
m.mb.last_addr = 1
@@ -1202,74 +1416,52 @@ def test_msg_modbus_rsp2(ConfigTsunInv1, MsgModbusResp20):
assert m.db.db == {}
m.new_data['inverter'] = False
- m.read() # read complete msg, and dispatch msg
- assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
- assert m.mb.err == 0
- assert m.msg_count == 1
- assert m._forward_buffer==MsgModbusResp20
- assert m._send_buffer==b''
- assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
- assert m.db.get_db_value(Register.VERSION) == 'V5.1.09'
- assert m.new_data['inverter'] == True
-
- m.new_data['inverter'] = False
- m.mb.req_pend = True
- m.read() # read complete msg, and dispatch msg
- assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
- assert m.mb.err == 0
- assert m.msg_count == 2
- assert m._forward_buffer==MsgModbusResp20
- assert m._send_buffer==b''
- assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
- assert m.db.get_db_value(Register.VERSION) == 'V5.1.09'
- assert m.new_data['inverter'] == False
-
- m.close()
-
-def test_msg_modbus_rsp3(ConfigTsunInv1, MsgModbusResp20):
- '''Modbus response with a valid Modbus request must be forwarded'''
- ConfigTsunInv1
- m = MemoryStream(MsgModbusResp20)
- m.append_msg(MsgModbusResp20)
-
- m.mb.rsp_handler = m.msg_forward
- m.mb.last_addr = 1
- m.mb.last_fcode = 3
- m.mb.last_len = 20
- m.mb.last_reg = 0x3008
- m.mb.req_pend = True
- m.mb.err = 0
-
- assert m.db.db == {}
- m.new_data['inverter'] = False
-
- m.read() # read complete msg, and dispatch msg
- assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
- assert m.mb.err == 0
- assert m.msg_count == 1
- assert m._forward_buffer==MsgModbusResp20
- assert m._send_buffer==b''
- assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
- assert m.db.get_db_value(Register.VERSION) == 'V5.1.09'
- assert m.new_data['inverter'] == True
- m.new_data['inverter'] = False
- assert m.mb.req_pend == False
-
m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
assert m.mb.err == 5
assert m.msg_count == 2
- assert m._forward_buffer==MsgModbusResp20
+ assert m._forward_buffer==msg_modbus_rsp20
assert m._send_buffer==b''
- assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
+ assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
assert m.db.get_db_value(Register.VERSION) == 'V5.1.09'
- assert m.new_data['inverter'] == False
+ assert m.db.get_db_value(Register.TS_GRID) == m._utc()
+ assert m.new_data['inverter'] == True
m.close()
-def test_msg_modbus_invalid(ConfigTsunInv1, MsgModbusInv):
- ConfigTsunInv1
- m = MemoryStream(MsgModbusInv, (0,), False)
+def test_msg_modbus_rsp3(config_tsun_inv1, msg_modbus_rsp21):
+ '''Modbus response with a valid Modbus request must be forwarded'''
+ config_tsun_inv1
+ m = MemoryStream(msg_modbus_rsp21)
+ m.append_msg(msg_modbus_rsp21)
+
+ m.mb.rsp_handler = m.msg_forward
+ m.mb.last_addr = 1
+ m.mb.last_fcode = 3
+ m.mb.last_len = 20
+ m.mb.last_reg = 0x3008
+ m.mb.req_pend = True
+ m.mb.err = 0
+
+ assert m.db.db == {}
+ m.new_data['inverter'] = False
+
+ m.read() # read complete msg, and dispatch msg
+ assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
+ assert m.mb.err == 5
+ assert m.msg_count == 2
+ assert m._forward_buffer==msg_modbus_rsp21
+ assert m._send_buffer==b''
+ assert m.db.db == {'inverter': {'Version': 'V5.1.0E', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}}
+ assert m.db.get_db_value(Register.VERSION) == 'V5.1.0E'
+ assert m.db.get_db_value(Register.TS_GRID) == m._utc()
+ assert m.new_data['inverter'] == True
+
+ m.close()
+
+def test_msg_modbus_invalid(config_tsun_inv1, msg_modbus_inv):
+ config_tsun_inv1
+ m = MemoryStream(msg_modbus_inv, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
m.read() # read complete msg, and dispatch msg
@@ -1281,16 +1473,16 @@ def test_msg_modbus_invalid(ConfigTsunInv1, MsgModbusInv):
assert m.msg_id==119
assert m.header_len==23
assert m.data_len==13
- assert m._forward_buffer==MsgModbusInv
+ assert m._forward_buffer==msg_modbus_inv
assert m._send_buffer==b''
assert m.db.stat['proxy']['Unknown_Ctrl'] == 1
assert m.db.stat['proxy']['Modbus_Command'] == 0
m.close()
-def test_msg_modbus_fragment(ConfigTsunInv1, MsgModbusResp20):
- ConfigTsunInv1
+def test_msg_modbus_fragment(config_tsun_inv1, msg_modbus_rsp20):
+ config_tsun_inv1
# receive more bytes than expected (7 bytes from the next msg)
- m = MemoryStream(MsgModbusResp20+b'\x00\x00\x00\x45\x10\x52\x31', (0,))
+ m = MemoryStream(msg_modbus_rsp20+b'\x00\x00\x00\x45\x10\x52\x31', (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0
m.mb.rsp_handler = m.msg_forward
@@ -1310,7 +1502,7 @@ def test_msg_modbus_fragment(ConfigTsunInv1, MsgModbusResp20):
assert m.msg_id == 119
assert m.header_len == 23
assert m.data_len == 50
- assert m._forward_buffer==MsgModbusResp20
+ assert m._forward_buffer==msg_modbus_rsp20
assert m._send_buffer == b''
assert m.mb.err == 0
assert m.modbus_elms == 20-1 # register 0x300d is unknown, so one value can't be mapped
@@ -1319,8 +1511,8 @@ def test_msg_modbus_fragment(ConfigTsunInv1, MsgModbusResp20):
m.close()
@pytest.mark.asyncio
-async def test_msg_build_modbus_req(ConfigTsunInv1, MsgModbusCmd):
- ConfigTsunInv1
+async def test_msg_build_modbus_req(config_tsun_inv1, msg_modbus_cmd):
+ config_tsun_inv1
m = MemoryStream(b'', (0,), True)
m.id_str = b"R170000000000001"
await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0, logging.DEBUG)
@@ -1334,7 +1526,7 @@ async def test_msg_build_modbus_req(ConfigTsunInv1, MsgModbusCmd):
assert 0 == m.send_msg_ofs
assert m._forward_buffer == b''
assert m._send_buffer == b''
- assert m.writer.sent_pdu == MsgModbusCmd
+ assert m.writer.sent_pdu == msg_modbus_cmd
m.writer.sent_pdu = bytearray(0) # clear send buffer for next test
m.test_exception_async_write = True
@@ -1344,37 +1536,83 @@ async def test_msg_build_modbus_req(ConfigTsunInv1, MsgModbusCmd):
assert m._send_buffer == b''
assert m.writer.sent_pdu == b''
m.close()
-'''
-def test_zombie_conn(ConfigTsunInv1, MsgInverterInd):
- ConfigTsunInv1
- tracer.setLevel(logging.DEBUG)
- start_val = MemoryStream._RefNo
- m1 = MemoryStream(MsgInverterInd, (0,))
- assert MemoryStream._RefNo == 1 + start_val
- assert m1.RefNo == 1 + start_val
- m2 = MemoryStream(MsgInverterInd, (0,))
- assert MemoryStream._RefNo == 2 + start_val
- assert m2.RefNo == 2 + start_val
- m3 = MemoryStream(MsgInverterInd, (0,))
- assert MemoryStream._RefNo == 3 + start_val
- assert m3.RefNo == 3 + start_val
- assert m1.state == m1.State.init
- assert m2.state == m2.State.init
- assert m3.state == m3.State.init
- m1.read() # read complete msg, and set unique_id
- assert m1.state == m1.State.up
- assert m2.state == m2.State.init
- assert m3.state == m3.State.init
- m2.read() # read complete msg, and set unique_id
- assert m1.state == m1.State.closed
- assert m2.state == m2.State.up
- assert m3.state == m3.State.init
- m3.read() # read complete msg, and set unique_id
- assert m1.state == m1.State.closed
- assert m2.state == m2.State.closed
- assert m3.state == m3.State.up
- m1.close()
- m2.close()
- m3.close()
-'''
\ No newline at end of file
+def test_modbus_no_polling(config_no_modbus_poll, msg_get_time):
+ config_no_modbus_poll
+ m = MemoryStream(msg_get_time, (0,))
+ m.db.stat['proxy']['Unknown_Ctrl'] = 0
+ m.modbus_polling = False
+ m.read() # read complete msg, and dispatch msg
+ assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
+ assert m.msg_count == 1
+ assert m.id_str == b"R170000000000001"
+ assert m.unique_id == 'R170000000000001'
+ assert int(m.ctrl)==145
+ assert m.msg_id==34
+ assert m.header_len==23
+ assert m.ts_offset==0
+ assert m.data_len==0
+ assert m._forward_buffer==msg_get_time
+ assert m._send_buffer==b'\x00\x00\x00\x1b\x10R170000000000001\x91"\x00\x00\x01\x89\xc6,_\x00'
+ assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
+ m.close()
+
+@pytest.mark.asyncio
+async def test_modbus_polling(config_tsun_inv1, msg_inverter_ind):
+ config_tsun_inv1
+ assert asyncio.get_running_loop()
+
+ m = MemoryStream(msg_inverter_ind, (0,))
+ assert asyncio.get_running_loop() == m.mb_timer.loop
+ m.db.stat['proxy']['Unknown_Ctrl'] = 0
+ assert m.mb_timer.tim == None
+ m.read() # read complete msg, and dispatch msg
+ assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
+ assert m.msg_count == 1
+ assert m.id_str == b"R170000000000001"
+ assert m.unique_id == 'R170000000000001'
+ assert int(m.ctrl)==145
+ assert m.msg_id==4
+ assert m.header_len==23
+ assert m.ts_offset==0
+ assert m.data_len==120
+ assert m._forward_buffer==msg_inverter_ind
+ assert m._send_buffer==b'\x00\x00\x00\x14\x10R170000000000001\x99\x04\x01'
+ assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
+
+ m._send_buffer = bytearray(0) # clear send buffer for next test
+ assert m.mb_timeout == 0.5
+ assert next(m.mb_timer.exp_count) == 0
+
+ await asyncio.sleep(0.5)
+ assert m.writer.sent_pdu==b'\x00\x00\x00 \x10R170000000000001pw\x00\x01\xa3(\x08\x01\x030\x00\x000J\xde'
+ assert m._send_buffer==b''
+
+ await asyncio.sleep(0.5)
+ assert m.writer.sent_pdu==b'\x00\x00\x00 \x10R170000000000001pw\x00\x01\xa3(\x08\x01\x030\x00\x000J\xde'
+ assert m._send_buffer==b''
+
+ await asyncio.sleep(0.5)
+ assert m.writer.sent_pdu==b'\x00\x00\x00 \x10R170000000000001pw\x00\x01\xa3(\x08\x01\x03\x20\x00\x00`N"'
+ assert m._send_buffer==b''
+ assert next(m.mb_timer.exp_count) == 4
+ m.close()
+
+def test_broken_recv_buf(config_tsun_allow_all, broken_recv_buf):
+ config_tsun_allow_all
+ m = MemoryStream(broken_recv_buf, (0,))
+ m.db.stat['proxy']['Unknown_Ctrl'] = 0
+ assert m.db.stat['proxy']['Invalid_Data_Type'] == 0
+ m.read() # read complete msg, and dispatch msg
+ assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
+ assert m.msg_count == 1
+ assert m.id_str == b"R170000000000001"
+ assert m.unique_id == 'R170000000000001'
+ assert m.msg_recvd[0]['ctrl']==145
+ assert m.msg_recvd[0]['msg_id']==4
+ assert m.msg_recvd[0]['header_len']==23
+ assert m.msg_recvd[0]['data_len']==1263
+ assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
+ assert m.db.stat['proxy']['Invalid_Data_Type'] == 1
+
+ m.close()
diff --git a/app/tests/timestamp_old.svg b/app/tests/timestamp_old.svg
deleted file mode 100644
index 739a420..0000000
--- a/app/tests/timestamp_old.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-
-Inverter Inverter Proxy Proxy Cloud Cloud MQTT-Broker MQTT-Broker ContactInd store Contact Info in proxy ContactRsp (Ok) getTimeReq ContactInd ContactRsp (Ok) getTimeReq TimeRsp (time) TimeRsp (time) set clock in inverter DataInd (ts:=time) DataRsp DataInd (ts) DataInd DataRsp DataInd (ts:=time) DataRsp DataInd (ts) DataInd DataRsp
\ No newline at end of file
diff --git a/app/tests/timestamp_old.yuml b/app/tests/timestamp_old.yuml
deleted file mode 100644
index 8f2e99c..0000000
--- a/app/tests/timestamp_old.yuml
+++ /dev/null
@@ -1,26 +0,0 @@
-// {type:sequence}
-// {generate:true}
-
-[Inverter]ContactInd>[Proxy]
-[Proxy]-[note: store Contact Info in proxy{bg:cornsilk}]
-[Proxy]ContactRsp (Ok).>[Inverter]
-
-[Inverter]getTimeReq>[Proxy]
-[Proxy]ContactInd>[Cloud]
-[Cloud]ContactRsp (Ok).>[Proxy]
-[Proxy]getTimeReq>[Cloud]
-[Cloud]TimeRsp (time).>[Proxy]
-[Proxy]TimeRsp (time).>[Inverter]
-[Inverter]-[note: set clock in inverter{bg:cornsilk}]
-
-[Inverter]DataInd (ts:=time)>[Proxy]
-[Proxy]DataRsp>[Inverter]
-[Proxy]DataInd (ts)>>[Cloud]
-[Proxy]DataInd>>[MQTT-Broker]
-[Cloud]DataRsp>>[Proxy]
-
-[Inverter]DataInd (ts:=time)>[Proxy]
-[Proxy]DataRsp>[Inverter]
-[Proxy]DataInd (ts)>>[Cloud]
-[Proxy]DataInd>>[MQTT-Broker]
-[Cloud]DataRsp>>[Proxy]
diff --git a/system_tests/test_tcp_socket.py b/system_tests/test_tcp_socket.py
index e2b64f8..00ec70f 100644
--- a/system_tests/test_tcp_socket.py
+++ b/system_tests/test_tcp_socket.py
@@ -1,8 +1,6 @@
# test_with_pytest.py and scapy
#
import pytest, socket, time
-#from scapy.all import *
-#from scapy.layers.inet import IP, TCP, TCP_client
def get_sn() -> bytes:
return b'R170000000000001'
@@ -120,9 +118,7 @@ def MsgOtaUpdateReq(): # Over the air update request from talent cloud
@pytest.fixture(scope="session")
def ClientConnection():
- #host = '172.16.30.7'
host = 'logger.talent-monitoring.com'
- #host = '127.0.0.1'
port = 5005
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((host, port))
@@ -132,9 +128,7 @@ def ClientConnection():
s.close()
def tempClientConnection():
- #host = '172.16.30.7'
host = 'logger.talent-monitoring.com'
- #host = '127.0.0.1'
port = 5005
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((host, port))
@@ -148,7 +142,6 @@ def test_open_close():
pass
except:
assert False
- assert True
def test_send_contact_info1(ClientConnection, MsgContactInfo, MsgContactResp):
s = ClientConnection
@@ -166,7 +159,7 @@ def test_send_contact_info2(ClientConnection, MsgContactInfo2, MsgContactInfo, M
s.sendall(MsgContactInfo2)
data = s.recv(1024)
except TimeoutError:
- assert True
+ pass
else:
assert False
@@ -198,7 +191,7 @@ def test_send_contact_resp(ClientConnection, MsgContactResp):
s.sendall(MsgContactResp)
data = s.recv(1024)
except TimeoutError:
- assert True
+ pass
else:
assert data == b''
diff --git a/system_tests/test_tcp_socket_v2.py b/system_tests/test_tcp_socket_v2.py
index 5e978de..b3521a8 100644
--- a/system_tests/test_tcp_socket_v2.py
+++ b/system_tests/test_tcp_socket_v2.py
@@ -3,9 +3,6 @@
import pytest, socket, time, os
from dotenv import load_dotenv
-#from scapy.all import *
-#from scapy.layers.inet import IP, TCP, TCP_client
-
load_dotenv()
SOLARMAN_SNR = os.getenv('SOLARMAN_SNR', '00000080')
@@ -111,10 +108,7 @@ def MsgInvalidInfo(): # Contact Info message wrong start byte
@pytest.fixture(scope="session")
def ClientConnection():
- #host = '172.16.30.7'
host = 'logger.talent-monitoring.com'
- #host = 'iot.talent-monitoring.com'
- #host = '127.0.0.1'
port = 10000
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((host, port))
@@ -131,10 +125,7 @@ def checkResponse(data, Msg):
def tempClientConnection():
- #host = '172.16.30.7'
host = 'logger.talent-monitoring.com'
- #host = 'iot.talent-monitoring.com'
- #host = '127.0.0.1'
port = 10000
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((host, port))
@@ -145,11 +136,10 @@ def tempClientConnection():
def test_open_close():
try:
- for s in tempClientConnection():
- pass
+ for _ in tempClientConnection():
+ pass # test generator tempClientConnection()
except:
assert False
- assert True
def test_conn_msg(ClientConnection,MsgContactInfo, MsgContactResp):
s = ClientConnection