Compare commits

..

248 Commits

Author SHA1 Message Date
Gunnar Skjold
fdfa6c1b52 Fixed MQTT JSON for prices (#1116) 2026-01-01 20:07:48 +01:00
Gunnar Skjold
4f1790a464 Added support for Iskraemeco IE.5 in Croatia (#1107)
* Added support for Croation Iskra

* Temp removed meterid

* Fixed HDLC block decoding

* Fixed context length

* Changing some stuff back

* Change some stuff back

* Final test

* Added debugging

* Updated selector for iskra dataformat

* Added fake test frame
2025-12-30 10:12:31 +01:00
Gunnar Skjold
ca4cef5233 Fixed empty timestamp in Home-Assistant JSON (#1105)
* Nullable timestamps for HA JSON

* Nullable timestamps for MQTT JSON
2025-12-30 10:05:39 +01:00
Gunnar Skjold
a0d7fd0d95 Fixed extraction of negative prices from server (#1104) 2025-12-30 10:04:55 +01:00
Gunnar Skjold
489dbf9254 Fixed IPv6 formatting (#1106) 2025-12-30 10:04:30 +01:00
Gunnar Skjold
a81aa11558 Added support for frames without checksum (#1108) 2025-12-29 13:25:36 +01:00
Gunnar Skjold
2e4a0fc0a3 Fixed price shift for non-CET price area (#1090)
* Fixed non-CET price presentation

* Added compiled version

* Updated fix for non cet

* Fixed! I think...
2025-12-11 11:42:17 +01:00
Gunnar Skjold
fc6e9e8085 Fixed ESP8266 memory issue with price decoding (#1089) 2025-12-11 11:37:52 +01:00
Gunnar Skjold
ad73821f1c Disable auto buffer size for HAN on ESP8266 (#1086) 2025-12-11 11:34:46 +01:00
Gunnar Skjold
98bf5b958f Fixed float infinity issue (#1087) 2025-12-11 11:34:15 +01:00
Gunnar Skjold
f323c5a4f6 Fixed building without remote debug (#1084) 2025-12-09 12:19:00 +01:00
Gunnar Skjold
ea91248e67 Changed MQTT client timeout setting for ESP8266 (#1077) 2025-12-05 15:37:23 +01:00
Gunnar Skjold
271ce2081f Fixed reboot loop for some meters (#1075) 2025-12-05 10:02:59 +01:00
Gunnar Skjold
8438020dbd Feature: Dump hex data from meter to MQTT (#1071)
* Send raw data debug to MQTT

* Publish hexdump to /data

* Sensor for /data, but it doesnt work
2025-12-01 14:02:07 +01:00
Gunnar Skjold
9252a810df Improve power stability when using MQTT (#1070)
* Changes to improve MQTT and power stability

* Re-added the memory leak fix

* Re-added the memory leak fix

* Stop client before deleting

* Fixed potential nullpointer
2025-12-01 10:01:20 +01:00
Gunnar Skjold
c0c696a55c Fixed default MQTT subscription (#1065) 2025-11-27 09:37:06 +01:00
Gunnar Skjold
ef70d39f70 Fixed what hours the fixed price is applied to (#1069) 2025-11-27 09:36:10 +01:00
Gunnar Skjold
1cf890dc26 Improvements for 2.5.0-rc3 (#1064)
* Various changes for 2.5.0-rc3

* Changed to official amsleser wiki
2025-11-21 12:40:13 +01:00
Gunnar Skjold
9d307e3192 Fixed premature cut on version string if release candidate (#1061) 2025-11-21 08:22:55 +01:00
Gunnar Skjold
61f0356a10 Added option to select firmware channel (#1060) 2025-11-13 15:14:52 +01:00
Gunnar Skjold
c648546b61 Added support for 15 minute price resolution (#1031)
* 15min prices WIP

* WIP more changes for 15min prices

* More work on 15min pricing

* Fixed some errors

* Some changes after testing

* Graphical changes for 15min pricing

* Adjustments on MQTT handlers after switching to 15min prices

* Reverted some MQTT changes

* Adapted HA integration for 15min pricing

* Adapted JSON payload for 15min

* Adjustments during testing

* Set default price interval

* Fixed refresh of price graph when data changes

* Bugfixes

* Fixed some issues with raw payload

* Adjustments for meter timestamp from Kamstrup

* Updated readme

* Added detailed breakdown of payloads coming from Norwegian meters

* Minor changes relating to price

* Fixed byte alignment on price config

* Changes to support RC upgraders
2025-11-13 15:10:54 +01:00
Gunnar Skjold
ffd8d46f2e Automatic reboot when MQTT is lost (#1058)
* Fixing board type overwrite, zmartcharge default issues and disabling entsoe

* Fixed Zmartcharge configuration issue

* Option to auto reboot if MQTT connection is lost
2025-11-06 18:26:40 +01:00
Gunnar Skjold
eefbc08222 Updated release workflow (#1054)
* Added workflow to release RC versions

* Added missing config to build file

* Made S3 bucket configurable from repo

* Updated release workflow
2025-11-04 13:40:54 +01:00
Gunnar Skjold
1a5b9542f4 Include device information when asking for new version (#1052) 2025-10-30 15:54:57 +01:00
Gunnar Skjold
19ff70782f Update for L&G in Austria (#1049)
* Fixing board type overwrite, zmartcharge default issues and disabling entsoe

* Fixed Zmartcharge configuration issue

* Support for LNG2 with 11 data points instead of 14
2025-10-23 08:17:25 +02:00
Gunnar Skjold
0dfd2d9022 Various bug fixes (#1041)
* Fixing board type overwrite, zmartcharge default issues and disabling entsoe

* Fixed Zmartcharge configuration issue
2025-10-16 08:50:03 +02:00
Gunnar Skjold
7a4ab77a83 Tooltip added to bar charts (#1022) 2025-10-02 13:55:31 +02:00
Gunnar Skjold
46cd8c6e68 Added hour to tariff peaks (#1028)
* Added hour to tariff peaks

* Some adjustments
2025-10-02 13:51:58 +02:00
Gunnar Skjold
c307103605 Added unique ID to HA firmware upgrade entity (#1027) 2025-10-02 13:23:36 +02:00
Gunnar Skjold
d9ec111458 Fixed duplicate deviceuid in HA when using ethernet (#1025) 2025-10-02 13:13:16 +02:00
Gunnar Skjold
e3a1aa78a9 Added tariff peaks and total month to MQTT JSON payload (#1026)
* Added tariff peaks to MQTT JSON payload

* Bugfix after testing
2025-10-02 13:11:28 +02:00
Gunnar Skjold
b06aa5f71b Improve power saving (#1024)
* Limit tasks in loop based on voltage

* Updated disconnect voltage limit

* Fixed 8266 build
2025-10-02 12:59:47 +02:00
Gunnar Skjold
6a75b0fe71 Command to request day or month plot data via MQTT (#1023) 2025-10-02 12:56:25 +02:00
Gunnar Skjold
e11fac3d11 Added deploy action for Localazy translations (#1014)
* Added deploy action for Localazy translations

* Fixed yml
2025-09-25 14:03:51 +02:00
Gunnar Skjold
031422f783 Show error when unknown data was received (#1013) 2025-09-25 12:20:31 +02:00
Gunnar Skjold
2ff8fddc14 Fixed incorrect enable state for ZmartCharge when no config (#1012) 2025-09-25 12:05:18 +02:00
Gunnar Skjold
e5d260ae3e Zmartcharge support (#1007)
* ZC initial implementation

* ZmartCharge

* Fixed zc bug

* Adjustments to ZmartCharge connection
2025-09-25 11:38:05 +02:00
Gunnar Skjold
633671851e Fixed Kamstrup timestamp parsing (#1011)
* Fixed kamstrup timestamp deviation

* Fixed check for time zone
2025-09-25 10:47:47 +02:00
Gunnar Skjold
69da9f9d48 Only allow board type overwrite from config if not set (#1010) 2025-09-25 10:47:35 +02:00
Gunnar Skjold
19f78126d6 Auto enable price fetch when region is changed (#1009) 2025-09-25 10:47:22 +02:00
Gunnar Skjold
d3cc92949a Trim leading and trailing whitespace (#1008) 2025-09-25 10:46:26 +02:00
Gunnar Skjold
f1089faab5 Maintian "no config" state after vendor config (#999) 2025-09-25 10:46:02 +02:00
Gunnar Skjold
4a3ad6ab9b Fixed /setup redirect after /vendor (#998) 2025-09-25 10:44:17 +02:00
Falke Carlsen
86449949c5 refactor: fix 'boundry' typo to 'boundary' (#982) 2025-09-25 10:43:57 +02:00
Gunnar Skjold
9bd9826835 Added GPIO 20 and 21 for ESP32-C3 (#979)
* Added GPIO 20 and 21 for C3

* Updated webapp after changing GPIO for C3
2025-09-25 10:39:55 +02:00
ArnieO
94ff9d6da7 Added Omnipower frame (#1002)
Kamstrup Omnipower frames from Bornholm, DK - with two decimals on the voltage reading. The customary is voltage without decimals. 
Received 9-sep-25.
2025-09-09 17:25:50 +02:00
ArnieO
9518d1811b Uploaded new *.raw file (#995)
From Landis&Gyr E350, HAN-NVE, Norway. Meter owner: https://klive.no/
2025-08-22 15:28:09 +02:00
ArnieO
983426b36c Add Poland docs (#987)
* Create Poland/Stoen folder structure

* Add PDF documents
2025-07-29 09:18:45 +02:00
Gunnar Skjold
bcb3c3b2ec Fixed price config with no dates (#974) 2025-06-05 11:47:36 +02:00
Gunnar Skjold
9fd383c1ef Updated release title (#973) 2025-06-05 08:05:50 +02:00
dependabot[bot]
a931f4cef8 Bump vite from 4.5.9 to 4.5.14 in /lib/SvelteUi/app (#972)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.9 to 4.5.14.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.14/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.14/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 4.5.14
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-05 07:42:29 +02:00
Gunnar Skjold
fddecaab39 Added meter multipliers to config file (#971) 2025-06-05 07:42:12 +02:00
Gunnar Skjold
e5eab82d68 Use list type 4 when phase power is included in Slovenian format (#967) 2025-06-05 07:41:50 +02:00
Gunnar Skjold
8ae1d46b2a Fixed DSMR timestamp parsing (#966)
* Fixed DSMR parsing and added support for more formats

* Add current time as package timestamp for DSMR
2025-06-05 07:41:36 +02:00
Gunnar Skjold
99ccb03b45 Added per phase power for L&G (#965)
* Added phase power parsing for Austrian L&G

* Use list type 4 when L&G phase power is present
2025-06-05 07:41:19 +02:00
Gunnar Skjold
b8f2d501a5 Increased kwh resultion in plot json (#961) 2025-06-05 07:40:57 +02:00
Gunnar Skjold
e042806619 Fixed multiplier problem for some L&G meters (#960) 2025-06-05 07:40:44 +02:00
Gunnar Skjold
16f9ed7ecb Fixed hour skew on price modifier (#959)
* Use local timezone in price config

* Additional changes for previous commit

* Use CET/CEST for energy prices collected from server
2025-06-05 07:40:29 +02:00
Gunnar Skjold
3eaefefd26 Fixed price modifier date inclusion (#958)
* Support price config end before start

* More changes to fix price modifiers

* More changes to fix price modifiers
2025-06-05 07:40:09 +02:00
Gunnar Skjold
03c8c3ddbc Fixed release name (#953) 2025-06-05 07:39:36 +02:00
dependabot[bot]
08371b9078 Bump http-proxy-middleware from 2.0.7 to 2.0.9 in /lib/SvelteUi/app (#950)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.7 to 2.0.9.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.9/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.7...v2.0.9)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-version: 2.0.9
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-05 07:39:14 +02:00
Gunnar Skjold
a6ae25f3e7 Changed way of creating release (#944) 2025-03-24 10:51:07 +01:00
dependabot[bot]
8051db6a9b Bump esbuild from 0.18.20 to 0.25.1 in /lib/SvelteUi/app (#943)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.18.20 to 0.25.1.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2023.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.18.20...v0.25.1)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 09:10:54 +01:00
Gunnar Skjold
d0bfdae5d8 Automatic release notes (#942) 2025-03-24 09:05:02 +01:00
Gunnar Skjold
4d681ed2e2 Support fw upgrade via MQTT with JSON payload (#941) 2025-03-24 09:00:05 +01:00
Gunnar Skjold
8ee3f53714 Fixed firmare upload when web context is defined (#938) 2025-03-24 08:59:52 +01:00
Gunnar Skjold
e8cf8a98ed Fixed baud/parity autodetect (#937) 2025-03-24 08:59:40 +01:00
Gunnar Skjold
9153a98694 Fix: #935 - Brownout reboot loop (#936) 2025-03-24 08:59:30 +01:00
Gunnar Skjold
37aa6ae816 Fix: #918 - MQTT/SSL does not reconnect after disconnect (#933) 2025-03-24 08:59:18 +01:00
Gunnar Skjold
8a35346fcf Fixed backup and restore of price modifiers (#925) (#932)
* Fix: #925 - Backup/restore of price modifiers

* Removed unused importy
2025-03-24 08:58:31 +01:00
Gunnar Skjold
792ae4c935 Fix: #926 - Historical data lost when upgrading ESP8266 between 2.4.x versions (#931) 2025-03-24 08:58:14 +01:00
Gunnar Skjold
a7324d828a Fixed nullpointer in upgrade 2025-02-14 18:51:07 +01:00
Gunnar Skjold
fe739c51d3 Cloud stuff 2025-02-14 18:04:12 +01:00
Gunnar Skjold
795d2d0375 Some debugging of cloud connection 2025-02-14 14:51:47 +01:00
Gunnar Skjold
5ef36a91f6 Updated esbuild dependency 2025-02-13 13:10:50 +01:00
Gunnar Skjold
8491d6c471 Changed to the latest possible node version 2025-02-13 13:07:57 +01:00
Gunnar Skjold
f95f22058a Merge branch 'main' of github.com:UtilitechAS/amsreader-firmware 2025-02-13 13:01:05 +01:00
Gunnar Skjold
b5c45cebfa Merge pull request #866 from saddfox/devcontainer
Add devcontainer
2025-02-13 12:55:57 +01:00
Gunnar Skjold
19a953b269 Updated workflows 2025-02-13 12:55:43 +01:00
Gunnar Skjold
6ae970ff68 Updated workflows 2025-02-13 12:45:43 +01:00
Gunnar Skjold
0f0ee82af9 Various bug fix for GUI 2025-02-13 12:33:54 +01:00
Gunnar Skjold
d84b9351e1 Fixed ESP8266 build 2025-02-03 18:02:20 +01:00
Gunnar Skjold
6668258b66 Support for update entity in HA 2025-02-03 17:51:36 +01:00
Gunnar Skjold
9c42aab04f Fixed extra price 2025-01-27 16:31:43 +01:00
Gunnar Skjold
c771870e3e Merge branch 'main' of github.com:UtilitechAS/amsreader-firmware 2025-01-27 16:08:02 +01:00
Gunnar Skjold
fb59ee52c1 Merge pull request #890 from UtilitechAS/dependabot/npm_and_yarn/lib/SvelteUi/app/nanoid-3.3.8
Bump nanoid from 3.3.7 to 3.3.8 in /lib/SvelteUi/app
2025-01-27 16:04:28 +01:00
Gunnar Skjold
3483910136 Merge pull request #907 from UtilitechAS/dependabot/npm_and_yarn/lib/SvelteUi/app/vite-4.5.9
Bump vite from 4.5.5 to 4.5.9 in /lib/SvelteUi/app
2025-01-27 16:04:16 +01:00
Gunnar Skjold
13e70f7bd4 Merge pull request #906 from dbeinder/nullterm
Reserve space for null terminator
2025-01-27 16:03:44 +01:00
Gunnar Skjold
210001e232 Prevent meterid override 2025-01-24 13:22:08 +01:00
Gunnar Skjold
094e588ad5 Strip non-ascii from meter id 2025-01-24 07:44:21 +01:00
Gunnar Skjold
f9d0cdfa47 Fixed nullpointer 2025-01-23 21:37:17 +01:00
Gunnar Skjold
0476058958 Fixed wifi scan for 8266 2025-01-23 14:46:52 +01:00
Gunnar Skjold
3e337a5639 Option to enter manual SSID 2025-01-23 14:37:28 +01:00
Gunnar Skjold
bf0e1d1bf3 Lib updates 2025-01-23 14:16:05 +01:00
Gunnar Skjold
d3b65b0175 SSID scan 2025-01-23 14:13:23 +01:00
Gunnar Skjold
2caa7252a0 Enforce pattern on all input fields 2025-01-23 12:01:39 +01:00
Gunnar Skjold
f7596de166 Fixed memory leak 2025-01-23 11:22:03 +01:00
Gunnar Skjold
f5178459e8 Fixed incorrect additional price in array 2025-01-22 19:59:41 +01:00
Gunnar Skjold
a55f7dc66a Fixed vcc reading on esp8266 2025-01-22 19:09:08 +01:00
Gunnar Skjold
3d9cad8953 Fixed invalid characters in meterId 2025-01-22 17:56:33 +01:00
Gunnar Skjold
67535b2792 Fixed cloud connection for C3 2025-01-22 17:49:54 +01:00
Gunnar Skjold
2b5f3f50df Fixed cloud connection for C3 2025-01-22 17:49:38 +01:00
Gunnar Skjold
111807a743 Web framework update 2025-01-22 17:32:41 +01:00
dependabot[bot]
348ba8cfc4 Bump vite from 4.5.5 to 4.5.9 in /lib/SvelteUi/app
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.5 to 4.5.9.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.9/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-22 06:43:57 +00:00
david-beinder
81c72f0ca3 Reserve space for null terminator 2025-01-18 01:11:24 +01:00
Gunnar Skjold
20de294a8c Updated build 2025-01-12 18:21:40 +01:00
Gunnar Skjold
e31faaa726 Added a DSMR frame as documentation 2025-01-12 17:59:26 +01:00
Gunnar Skjold
4ad2921132 Fixed config reload for HA and Domoticz if changed 2025-01-12 12:01:16 +01:00
Gunnar Skjold
1fa62fa97c Fixed aidon package timestamp 2025-01-12 10:32:14 +01:00
Gunnar Skjold
affa66f78f Allow special characters in mqtt user/pass 2025-01-12 10:23:57 +01:00
Gunnar Skjold
84860bc684 Failsafe on combining tariff counters 2025-01-12 10:16:19 +01:00
Gunnar Skjold
0cadedf9df Fixed red blink after 49 days 2025-01-12 10:05:57 +01:00
Gunnar Skjold
0c3eea8d37 Added timestamp sensors to HA 2025-01-12 09:57:29 +01:00
Gunnar Skjold
1d4e7e564d Fixed timestamp bug 2025-01-09 15:35:22 +01:00
Gunnar Skjold
067aa0c6cb More changes for slovakian iskra 2024-12-23 11:57:32 +01:00
dependabot[bot]
3d540e2a65 Bump nanoid from 3.3.7 to 3.3.8 in /lib/SvelteUi/app
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-17 12:48:44 +00:00
Gunnar Skjold
61d4728ffa DSMR with encrypted payload 2024-12-14 10:54:02 +01:00
Gunnar Skjold
42e0ca963c Merge branch 'main' into pnu 2024-12-12 10:51:52 +01:00
Gunnar Skjold
3bc40cb136 Some adjustments for Iskra 2024-12-12 09:44:10 +01:00
Gunnar Skjold
1453bbbb7e Adjustments on auto updater for 8266 2024-12-07 21:45:47 +01:00
Gunnar Skjold
9da2d0760e More adjustments for auto update 2024-12-07 11:32:45 +01:00
Gunnar Skjold
18af98511a Some adjustments 2024-12-05 20:10:44 +01:00
Gunnar Skjold
40c84d2429 Fixed firmware upload from GUI 2024-12-05 19:15:17 +01:00
Gunnar Skjold
c0d95f918e Merge branch 'main' into pnu 2024-12-05 18:15:37 +01:00
Gunnar Skjold
0947d335ce Fixed unprocessable characters in json 2024-12-05 14:20:12 +01:00
Gunnar Skjold
43e2b2478b Added more for Slovenian Iskra 2024-12-05 13:57:33 +01:00
Gunnar Skjold
a5fc1f0cbe New firmware upload 2024-12-05 13:40:30 +01:00
Gunnar Skjold
bf3059ba04 Now moving LittleFS in a better way 2024-12-04 20:37:56 +01:00
Gunnar Skjold
319b534d4f Improved firmware update 2024-11-29 16:44:56 +01:00
Gunnar Skjold
b7c2510098 Store upgrade progress to eeprom 2024-11-29 11:26:00 +01:00
Gunnar Skjold
07205b8008 Merge branch 'main' into pnu
# Conflicts:
#	lib/HwTools/src/HwTools.cpp
#	lib/SvelteUi/app/dist/index.css
#	lib/SvelteUi/app/dist/index.js
#	lib/SvelteUi/app/vite.config.js
2024-11-29 08:05:49 +01:00
saddfox
165a385844 add devcontainer 2024-11-10 18:09:18 +01:00
Gunnar Skjold
fa76233151 Added missing code for setting mqtt keep alive 2024-11-09 15:44:30 +01:00
Gunnar Skjold
22b7877ee9 Fixed build error 2024-11-09 15:29:19 +01:00
Gunnar Skjold
bd9a1bb330 Some frame data 2024-11-09 15:17:04 +01:00
Gunnar Skjold
1b5efaa937 Fixed some labels 2024-11-09 15:16:53 +01:00
Gunnar Skjold
05ce4c5c1a Ability to change MQTT timeout and keepalive 2024-11-09 15:12:54 +01:00
Gunnar Skjold
e022f630b2 Removed UDP as recommended cloud connection method 2024-11-07 09:55:15 +01:00
Gunnar Skjold
a73ff87653 Removed UDP as recommended cloud connection method 2024-11-07 09:37:05 +01:00
Gunnar Skjold
70d6a8699b Added default settings to KMP communicator 2024-11-06 18:22:56 +01:00
Gunnar Skjold
118c633878 Fixed autodetect 2024-11-06 18:10:38 +01:00
Gunnar Skjold
fa5985f60b Prevent redirect loop when changing from context to empty 2024-11-04 08:41:34 +01:00
Gunnar Skjold
c1309e9a13 Add package timestamp if missing 2024-11-03 11:28:03 +01:00
Gunnar Skjold
718eef7999 Fixed configfile download when using context path 2024-11-03 11:21:35 +01:00
Gunnar Skjold
afdd282adf Make client id required 2024-11-03 10:43:27 +01:00
Gunnar Skjold
1302f18fab Adjustments to make IPv6 address visible in GUI 2024-11-03 10:21:13 +01:00
Gunnar Skjold
fc89670ea4 Manually merged changes from pull #826 2024-11-03 10:07:51 +01:00
Gunnar Skjold
225320ea7d git branchMerge branch 'main' of github.com:UtilitechAS/amsreader-firmware 2024-11-03 10:02:00 +01:00
Gunnar Skjold
b2cf3bb0c4 Merge pull request #847 from UtilitechAS/dependabot/npm_and_yarn/lib/SvelteUi/app/rollup-3.29.5
Bump rollup from 3.29.4 to 3.29.5 in /lib/SvelteUi/app
2024-11-03 09:59:23 +01:00
Gunnar Skjold
e36a8ade42 Merge pull request #848 from ackjewtn/add_wesp32_poe_board
Add support for wESP32-PoE board
2024-11-03 09:59:13 +01:00
Gunnar Skjold
2ef67d92c4 Fixed missing prices after changes in entsoe API 2024-11-03 09:51:54 +01:00
Peter Wahlberg
1c9af6b1c2 Revert formatting 2024-09-27 17:17:02 +02:00
Peter Wahlberg
4f47ffa21e Add support for wESP32-PoE board 2024-09-27 17:01:30 +02:00
dependabot[bot]
3d1026f829 Bump rollup from 3.29.4 to 3.29.5 in /lib/SvelteUi/app
Bumps [rollup](https://github.com/rollup/rollup) from 3.29.4 to 3.29.5.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v3.29.4...v3.29.5)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-27 05:02:27 +00:00
Gunnar Skjold
af6ecc5d47 Adjustments of clock parsing from dlms 2024-09-21 20:03:46 +02:00
Gunnar Skjold
025c556626 More relaxed on when hourly data is received 2024-09-21 16:04:05 +02:00
Gunnar Skjold
0b342d6c49 Some changes to data update 2024-09-21 13:00:22 +02:00
Gunnar Skjold
ada57b1052 Trying to fix data reset issue 2024-09-19 12:54:58 +02:00
Gunnar Skjold
cf773985b1 Merge pull request #836 from UtilitechAS/dependabot/npm_and_yarn/lib/SvelteUi/app/svelte-4.2.19
Bump svelte from 3.59.2 to 4.2.19 in /lib/SvelteUi/app
2024-09-19 12:39:39 +02:00
Gunnar Skjold
75a14ad9cb Merge pull request #840 from ruffy91/patch-1
Added kWH multiplier to phases
2024-09-19 12:39:26 +02:00
Fabian
fbe90cbc5a Update IEC6205675.cpp
kWh multiplier also applies to all the phases, not only total energy
2024-09-04 21:38:20 +02:00
dependabot[bot]
f0bc895952 Bump svelte from 3.59.2 to 4.2.19 in /lib/SvelteUi/app
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 3.59.2 to 4.2.19.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/svelte@4.2.19/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@4.2.19/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-30 17:21:59 +00:00
Gunnar Skjold
f5c10c1a50 Send price config to cloud 2024-08-24 21:22:43 +02:00
Gunnar Skjold
c4736c730e Send price config to cloud 2024-08-24 20:55:34 +02:00
Gunnar Skjold
d2c96c733c Send price config to cloud 2024-08-24 09:40:58 +02:00
Gunnar Skjold
d0dd9de4d7 Updated fix for missing graph data 2024-08-24 08:37:24 +02:00
Gunnar Skjold
159244974d Trying to fix issue 2024-08-20 20:55:26 +02:00
Gunnar Skjold
933926fbb4 Fixed debug logging 2024-08-15 20:02:35 +02:00
Gunnar Skjold
2c95f036ba Fixed power factor sensors in HA 2024-08-15 19:44:24 +02:00
Gunnar Skjold
c5c0b52eb7 Try to fix day clearing issue 2024-08-15 19:33:30 +02:00
Gunnar Skjold
561b62cafe Cloud configuration 2024-08-15 19:18:10 +02:00
Gunnar Skjold
55520cd7f6 GUI package upgrades 2024-08-06 20:54:39 +02:00
Gunnar Skjold
e27d41839e Merge branch 'main' of github.com:UtilitechAS/amsreader-firmware 2024-08-06 20:52:41 +02:00
Gunnar Skjold
569a0ddfaa Some final adjustments 2024-08-06 20:51:02 +02:00
Gunnar Skjold
de63263dbb Fixed date skew when computer is on different timezone 2024-08-02 22:34:53 -08:00
Gunnar Skjold
8adb7055c0 Fixed date skew when computer is on different timezone 2024-08-02 19:02:00 -11:00
Gunnar Skjold
11e09c5b56 Fixed undefined month 2024-08-03 07:32:10 +02:00
Gunnar Skjold
ac32f74eba Bugfix 2024-08-02 19:59:30 +02:00
Gunnar Skjold
d49b8c3839 Option to disable UART TX 2024-08-02 19:59:16 +02:00
Gunnar Skjold
054fd43a0d Initial implementation of custom upgrade code 2024-08-02 17:56:39 +02:00
ArnieO
423c5da25e Uploaded overview of meters / interfaces 2024-07-17 09:11:04 +02:00
Gunnar Skjold
9a4a8a10f2 Corrected RSA block size 2024-07-14 21:31:17 +02:00
Gunnar Skjold
d15992142e Removed test ip 2024-07-14 21:24:29 +02:00
Gunnar Skjold
fed3948f85 Corrected RSA block size 2024-07-14 21:23:45 +02:00
Gunnar Skjold
b77342a648 Some cloud changes 2024-07-14 21:07:50 +02:00
Gunnar Skjold
04f407aba0 Stability improvements 2024-07-11 17:48:50 +02:00
Gunnar Skjold
b122fae04c Updated license headers 2024-07-11 14:03:00 +02:00
Gunnar Skjold
8a4efd0047 Restructuring to be able to include precompiled kmp lib 2024-07-11 13:15:22 +02:00
Gunnar Skjold
4407526d96 Changed order of if cases 2024-07-03 09:56:08 +02:00
Gunnar Skjold
1d91416348 Fix for meters with clock skew 2024-07-03 09:22:09 +02:00
Gunnar Skjold
a1bbcc20e3 Fixed 8266 build 2024-06-29 10:24:38 +02:00
Gunnar Skjold
2b779920d7 Fixed issue when graphs go blank if meter clock is running fast 2024-06-29 10:18:24 +02:00
Gunnar Skjold
de79fd4c43 Send prices to MQTT when having fixed price only 2024-06-29 10:15:15 +02:00
Gunnar Skjold
8dfaa34d03 Buffer adjustments for 8266 2024-06-29 10:14:43 +02:00
Gunnar Skjold
0d0dc07903 Some changes for cloud service 2024-06-29 10:13:41 +02:00
Gunnar Skjold
e38a064928 Fixed timezone issue in DLMS timestamps 2024-06-27 10:54:22 +02:00
Gunnar Skjold
81b3aacc4d Fixed debug 2024-06-15 09:21:12 +02:00
Gunnar Skjold
6277d5880d Updated frontened 2024-06-10 17:28:34 +02:00
Gunnar Skjold
8424d1f75e Set threephase when applying OBIS codes 2024-06-09 10:21:55 +02:00
Gunnar Skjold
099b23d2d5 Merge branch 'main' of github.com:UtilitechAS/amsreader-firmware 2024-06-07 19:13:21 +02:00
Gunnar Skjold
28d746415c Remote KMP as selection in config 2024-06-07 19:13:03 +02:00
Gunnar Skjold
fd373a5846 Disable remote debug for C3 2024-06-07 19:12:33 +02:00
Gunnar Skjold
cd7315990f Build option to enable KMP 2024-06-07 18:55:37 +02:00
Gunnar Skjold
59bf0ce066 Build option to disable remote debug 2024-06-07 18:48:26 +02:00
Gunnar Skjold
93e55f457a Tariff thresholds on MQTT 2024-06-06 21:38:03 +02:00
Gunnar Skjold
4d340c5482 Use estimated if Aidon sends same value as last hour 2024-06-06 21:37:45 +02:00
Gunnar Skjold
1db0082103 Added export price sensor to HA payload 2024-06-06 18:09:33 +02:00
Gunnar Skjold
f593e14e68 Combined import/export in data.json 2024-06-06 17:34:33 +02:00
Gunnar Skjold
da6328c246 Fixed HA hostname ESP8266 build issue 2024-06-06 17:17:59 +02:00
Gunnar Skjold
5eeab2ba89 Fixed amperage plot 2024-06-06 15:55:43 +02:00
Gunnar Skjold
eeaaf088ac Fixed issues with HA sensors 2024-06-06 15:41:21 +02:00
Gunnar Skjold
32afea2817 Iskra 2024-06-04 20:33:03 +02:00
Gunnar Skjold
e7b496280d KMP stuff 2024-06-03 18:09:40 +02:00
Gunnar Skjold
d0621e98cd Fixed blank webpage 2024-05-30 14:07:45 +02:00
Gunnar Skjold
202d57843c Iskra case fix? 2024-05-30 13:28:50 +02:00
Gunnar Skjold
770d662f2c Changes to allow interaction with cloud service 2024-05-30 09:46:45 +02:00
Gunnar Skjold
da1d5993f4 Merge pull request #796 from devdems/main
Add Slovenia
2024-05-23 23:34:06 +02:00
Gunnar Skjold
7ad97daf86 Some cleanup 2024-05-23 22:20:01 +02:00
Gunnar Skjold
3b155d78b1 Removed som debugging 2024-05-23 21:58:17 +02:00
Gunnar Skjold
37a1e3b93e Another iskra special case 2024-05-23 20:57:32 +02:00
Gunnar Skjold
846cf85331 Use export price for export calculations 2024-05-23 20:33:13 +02:00
Gunnar Skjold
a1a6953521 Use export price for export cost calculations 2024-05-23 20:21:56 +02:00
Gunnar Skjold
a137316cec Added voltage check for all Pow* devices 2024-05-23 19:40:28 +02:00
Gunnar Skjold
faa468b287 Fixed loading order of json and when to display price graph 2024-05-23 19:12:38 +02:00
Gunnar Skjold
703c68a2cf Fixed autodetect 2024-05-23 18:58:20 +02:00
Gunnar Skjold
b6168a0082 Fixed 404 2024-05-23 17:35:17 +02:00
Gunnar Skjold
38ec99f2ee Move price elements 2024-05-23 17:34:26 +02:00
Gunnar Skjold
22f8349f2f Fixed bugs after testing 2024-05-23 17:13:32 +02:00
Gunnar Skjold
d98a021f39 Edit day and month data 2024-05-23 15:17:19 +02:00
Gunnar Skjold
77d899fe0a Fixed month in clock 2024-05-23 14:06:42 +02:00
Gunnar Skjold
c03bfcbcd7 Auto change buffer size based on baud rate when autodetect 2024-05-23 14:03:16 +02:00
Gunnar Skjold
f3732674b0 Fixed autodetect and enabled for Pow-K (To handle when configured as P1) 2024-05-23 13:59:16 +02:00
devdems
5926ddfaf9 Update ConfigurationPanel.svelte
Add Slovenia
2024-05-21 16:07:20 +02:00
Gunnar Skjold
99e341e582 Changed buffer size defaults and some null terminator issues on json mqtt 2024-05-01 07:38:30 +02:00
Gunnar Skjold
05506cdc9f Fixed ESP8266 build issue 2024-04-21 10:35:30 +02:00
Gunnar Skjold
3f1861deda MQTT on interval 2024-04-21 10:21:39 +02:00
Gunnar Skjold
efacbd4b31 Code restructuring 2024-04-21 08:43:59 +02:00
Gunnar Skjold
beaaa191e8 Color fix 2024-04-20 13:08:39 +02:00
Gunnar Skjold
43d49f21a3 Changed GPIO dropdowns 2024-04-20 10:51:27 +02:00
Gunnar Skjold
95c495b773 Some changes for cloud service 2024-04-20 09:13:41 +02:00
Gunnar Skjold
68c680debf Set TX pin low if passive communicator 2024-04-19 14:51:27 +02:00
Gunnar Skjold
fec6cc7612 Updated HA state class on realtime sensors 2024-04-19 08:48:57 +02:00
Gunnar Skjold
dc3cea80b6 Fixed HA state class on realtime sensors 2024-04-15 19:54:40 +02:00
Gunnar Skjold
ce67ef2bea Show current export price in export gauge 2024-04-14 14:47:53 +02:00
Gunnar Skjold
3a980fac4c Fixed price config 2024-04-14 14:20:46 +02:00
Gunnar Skjold
77873f4a38 Fixed mixup of last month cost and kwh 2024-04-14 13:33:11 +02:00
Gunnar Skjold
d9d384ea02 Fixed code 2024-04-14 13:30:46 +02:00
Gunnar Skjold
16fb1ea87c Fixed context bug 2024-04-14 13:24:20 +02:00
171 changed files with 13616 additions and 6019 deletions

View File

@@ -0,0 +1,31 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu
{
"name": "amsreader-devcontainer",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/base:jammy",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "19",
"pnpmVersion": "none",
"nvmVersion": "latest"
},
"ghcr.io/devcontainers/features/python:1": {
"version": "3.9"
}
},
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": ".devcontainer/postCreateCommand.sh",
// Configure tool-specific properties.
"customizations": {
"vscode": {
"extensions": [
"platformio.platformio-ide",
"ms-vscode.cpptools",
"svelte.svelte-vscode"
]
}
}
}

View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -e
# Upgrade pip
python -m pip install --upgrade pip
# Install Python packages
pip install -U platformio css_html_js_minify
# Navigate to the Svelte app directory
cd lib/SvelteUi/app
# Install npm dependencies and build the app
npm ci
npm run build
# Return to the previous directory
cd -

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
* text=auto eol=lf
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf

View File

@@ -27,15 +27,16 @@ jobs:
run: |
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_SC_KEY/AMS2MQTT_SC_KEY=\\"${{secrets.AMS2MQTT_SC_KEY}}\\"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_USER/ENERGY_SPEEDOMETER_USER=\\"${{secrets.ENERGY_SPEEDOMETER_USER}}\\"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_PASS/ENERGY_SPEEDOMETER_PASS=\\"${{secrets.ENERGY_SPEEDOMETER_PASS}}\\"/g' platformio.ini
- name: Cache Python dependencies
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('platformio.ini') }}
- name: Cache PlatformIO dependencies
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.pio/libdeps
key: ${{ runner.os }}-pio-${{ hashFiles('platformio.ini') }}
@@ -50,7 +51,7 @@ jobs:
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: '16.x'
node-version: '19.x'
- name: Build with node
run: |
cd lib/SvelteUi/app

41
.github/workflows/localazy.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Deploy language files from localazy
on:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out code from repo
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-north-1
- name: Generate localazy-keys.json
run: |
echo '{"writeKey": "", "readKey": "${{secrets.LOCALAZY_READ_KEY}}"}' > localazy/localazy-keys.json
- name: Create localazy language folder
run: mkdir -p localazy/language
- name: Install Localazy CLI
run: npm install -g @localazy/cli
- name: Download translations
working-directory: localazy
run: localazy download -k localazy-keys.json
- name: Upload translations to S3
run: aws s3 sync ./localazy/language/ s3://${{ secrets.AWS_S3_BUCKET }}/language/

85
.github/workflows/prerelease.yml vendored Normal file
View File

@@ -0,0 +1,85 @@
name: Release candidate build and upload
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+'
jobs:
prepare:
runs-on: ubuntu-latest
steps:
- name: Check out code from repo
uses: actions/checkout@v4
- name: Get release version for filenames
id: release_tag
env:
GITHUB_REF: ${{ github.ref }}
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11})
- name: Create release with release notes
id: create_release
uses: ncipollo/release-action@v1
with:
name: Release candidate v${{ steps.release_tag.outputs.tag }}
prerelease: true
outputs:
version: ${{ steps.release_tag.outputs.tag }}
upload_url: ${{ steps.create_release.outputs.upload_url }}
esp32s2:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32s2
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
subfolder: /rc
esp32s3:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32s3
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
subfolder: /rc
esp32c3:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32c3
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
subfolder: /rc
esp32:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
subfolder: /rc
esp32solo:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32solo
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
subfolder: /rc
esp8266:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp8266
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
subfolder: /rc
is_esp32: false

136
.github/workflows/release-deploy-env.yml vendored Normal file
View File

@@ -0,0 +1,136 @@
name: Build with env and deploy
on:
workflow_call:
inputs:
env:
description: 'The environment to build for'
required: true
type: string
upload_url:
description: 'The upload URL for the release assets'
required: true
type: string
version:
description: 'The version tag for the release assets'
required: true
type: string
subfolder:
description: 'The subfolder in S3 to upload the binary to'
required: false
type: string
default: ''
is_esp32:
description: 'Whether the build is for ESP32 based firmware'
required: false
type: boolean
default: true
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Check out code from repo
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-north-1
- name: Cache Python dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('platformio.ini') }}
- name: Cache PlatformIO dependencies
uses: actions/cache@v4
with:
path: ~/.pio/libdeps
key: ${{ runner.os }}-pio-${{ hashFiles('platformio.ini') }}
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Inject secrets into ini file
run: |
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_SC_KEY/AMS2MQTT_SC_KEY=\\"${{secrets.AMS2MQTT_SC_KEY}}\\"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_USER/ENERGY_SPEEDOMETER_USER=\\"${{secrets.ENERGY_SPEEDOMETER_USER}}\\"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_PASS/ENERGY_SPEEDOMETER_PASS=\\"${{secrets.ENERGY_SPEEDOMETER_PASS}}\\"/g' platformio.ini
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -U platformio css_html_js_minify
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: '19.x'
- name: Build with node
run: |
cd lib/SvelteUi/app
npm ci
npm run build
cd -
env:
CI: false
- name: PlatformIO lib install
run: pio lib install
- name: Build firmware
env:
GITHUB_TAG: v${{ inputs.version }}
run: pio run -e ${{ inputs.env }}
- name: Create zip file
run: /bin/sh scripts/${{ inputs.env }}/mkzip.sh
- name: Upload binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ inputs.upload_url }}
asset_path: .pio/build/${{ inputs.env }}/firmware.bin
asset_name: ams2mqtt-${{ inputs.env }}-${{ inputs.version }}.bin
asset_content_type: application/octet-stream
- name: Upload zip to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ inputs.upload_url }}
asset_path: ${{ inputs.env }}.zip
asset_name: ams2mqtt-${{ inputs.env }}-${{ inputs.version }}.zip
asset_content_type: application/zip
- name: Create MD5 checksum file
run: md5sum .pio/build/${{ inputs.env }}/firmware.bin | cut -z -d ' ' -f 1 > firmware.md5
- name: Upload binary to S3
run: aws s3 cp .pio/build/${{ inputs.env }}/firmware.bin s3://${{ secrets.AWS_S3_BUCKET }}/firmware${{ inputs.subfolder }}/ams2mqtt-${{ inputs.env }}-${{ inputs.version }}.bin
- name: Upload MD5 checksum to S3
run: aws s3 cp firmware.md5 s3://${{ secrets.AWS_S3_BUCKET }}/firmware${{ inputs.subfolder }}/ams2mqtt-${{ inputs.env }}-${{ inputs.version }}.md5
- name: Upload bootloader to S3
if: ${{ inputs.is_esp32 }}
run: aws s3 cp .pio/build/${{ inputs.env }}/bootloader.bin s3://${{ secrets.AWS_S3_BUCKET }}/firmware${{ inputs.subfolder }}/ams2mqtt-${{ inputs.env }}-${{ inputs.version }}-bootloader.bin
- name: Upload partition table to S3
if: ${{ inputs.is_esp32 }}
run: aws s3 cp .pio/build/${{ inputs.env }}/partitions.bin s3://${{ secrets.AWS_S3_BUCKET }}/firmware${{ inputs.subfolder }}/ams2mqtt-${{ inputs.env }}-${{ inputs.version }}-partitions.bin
- name: Upload app0 to S3
if: ${{ inputs.is_esp32 }}
run: aws s3 cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin s3://${{ secrets.AWS_S3_BUCKET }}/firmware${{ inputs.subfolder }}/ams2mqtt-${{ inputs.env }}-${{ inputs.version }}-app0.bin

View File

@@ -1,217 +1,79 @@
name: Release
name: Release build and upload
on:
push:
tags:
- 'v*.*.*'
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
build:
prepare:
runs-on: ubuntu-latest
steps:
- name: Check out code from repo
uses: actions/checkout@v1
- name: Get release version for filenames
id: release_tag
env:
GITHUB_REF: ${{ github.ref }}
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11})
- name: Get release version for code
env:
GITHUB_REF: ${{ github.ref }}
run: echo "GITHUB_TAG=$(echo ${GITHUB_REF##*/})" >> $GITHUB_ENV
- name: Check out code from repo
uses: actions/checkout@v4
- name: Get release version for filenames
id: release_tag
env:
GITHUB_REF: ${{ github.ref }}
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11})
- name: Inject secrets into ini file
run: |
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_SC_KEY/AMS2MQTT_SC_KEY=\\"${{secrets.AMS2MQTT_SC_KEY}}\\"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_USER/ENERGY_SPEEDOMETER_USER=\\"${{secrets.ENERGY_SPEEDOMETER_USER}}\\"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_PASS/ENERGY_SPEEDOMETER_PASS=\\"${{secrets.ENERGY_SPEEDOMETER_PASS}}\\"/g' platformio.ini
- name: Create release with release notes
id: create_release
uses: ncipollo/release-action@v1
with:
name: Release v${{ steps.release_tag.outputs.tag }}
generateReleaseNotes: true
- name: Cache Python dependencies
uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('platformio.ini') }}
- name: Cache PlatformIO dependencies
uses: actions/cache@v1
with:
path: ~/.pio/libdeps
key: ${{ runner.os }}-pio-${{ hashFiles('platformio.ini') }}
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -U platformio css_html_js_minify
outputs:
version: ${{ steps.release_tag.outputs.tag }}
upload_url: ${{ steps.create_release.outputs.upload_url }}
- name: Set up node
uses: actions/setup-node@v1
with:
node-version: '16.x'
- name: Build with node
run: |
cd lib/SvelteUi/app
npm ci
npm run build
cd -
env:
CI: false
- name: PlatformIO lib install
run: pio lib install
- name: Create Release
id: create_release
uses: actions/create-release@v1.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: Build esp8266 firmware
run: pio run -e esp8266
- name: Create esp8266 zip file
run: /bin/sh scripts/esp8266/mkzip.sh
- name: Upload esp8266 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp8266/firmware.bin
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp8266 zip to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: esp8266.zip
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32 firmware
run: pio run -e esp32
- name: Create esp32 zip file
run: /bin/sh scripts/esp32/mkzip.sh
- name: Upload esp32 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp32/firmware.bin
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32 zip to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: esp32.zip
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32s2 firmware
run: pio run -e esp32s2
- name: Create esp32s2 zip file
run: /bin/sh scripts/esp32s2/mkzip.sh
- name: Upload esp32s2 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp32s2/firmware.bin
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32s2 zip to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: esp32s2.zip
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32s3 firmware
run: pio run -e esp32s3
- name: Create esp32s3 zip file
run: /bin/sh scripts/esp32s3/mkzip.sh
- name: Upload esp32s3 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp32s3/firmware.bin
asset_name: ams2mqtt-esp32s3-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32s3 zip to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: esp32s3.zip
asset_name: ams2mqtt-esp32s3-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32solo firmware
run: pio run -e esp32solo
- name: Create esp32solo zip file
run: /bin/sh scripts/esp32solo/mkzip.sh
- name: Upload esp32solo binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp32solo/firmware.bin
asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32solo zip to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: esp32solo.zip
asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32c3 firmware
run: pio run -e esp32c3
- name: Create esp32c3 zip file
run: /bin/sh scripts/esp32c3/mkzip.sh
- name: Upload esp32c3 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp32c3/firmware.bin
asset_name: ams2mqtt-esp32c3-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32c3 zip to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: esp32c3.zip
asset_name: ams2mqtt-esp32c3-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
esp32s2:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32s2
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
esp32s3:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32s3
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
esp32c3:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32c3
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
esp32:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
esp32solo:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp32solo
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
esp8266:
needs: prepare
uses: ./.github/workflows/release-deploy-env.yml
secrets: inherit
with:
env: esp8266
version: ${{ needs.prepare.outputs.version }}
upload_url: ${{ needs.prepare.outputs.upload_url }}
is_esp32: false

View File

@@ -1,63 +0,0 @@
name: Test ESP8266
on:
workflow_dispatch:
jobs:
esp8266:
runs-on: esp8266
steps:
- uses: actions/checkout@v4
- name: Get commit hash
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Check outputs
run: echo ${{ steps.vars.outputs.sha_short }}
- name: Inject secrets into ini file
run: |
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_USER/ENERGY_SPEEDOMETER_USER=\\"${{secrets.ENERGY_SPEEDOMETER_USER}}\\"/g' platformio.ini
sed -i 's/NO_ENERGY_SPEEDOMETER_PASS/ENERGY_SPEEDOMETER_PASS=\\"${{secrets.ENERGY_SPEEDOMETER_PASS}}\\"/g' platformio.ini
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -U platformio css_html_js_minify
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: '16.x'
- name: Configure PlatformIO environment
run: |
echo "[platformio]
default_envs = dev8266
[env:dev8266]
platform = espressif8266@4.2.0
framework = arduino
board = esp12e
board_build.ldscript = eagle.flash.4m2m.ld
build_flags = \${common.build_flags}
lib_ldf_mode = off
lib_compat_mode = off
lib_deps = ESP8266WiFi, ESP8266mDNS, ESP8266WebServer, ESP8266HTTPClient, ESP8266httpUpdate, ESP8266SSDP, \${common.lib_deps}
lib_ignore = \${common.lib_ignore}
extra_scripts = \${common.extra_scripts}" > platformio-user.ini
- name: Build with node
run: |
cd lib/SvelteUi/app
npm ci
npm run build
cd -
env:
CI: true
- name: PlatformIO lib install
run: pio pkg update
- name: PlatformIO run
run: pio run -t upload --upload-port /dev/ttyUSB0
- name: Wait for device to come online
run: waitforhost 10.42.0.11 80
- name: Confirm version
run: curl -s http://10.42.0.11/sysinfo.json|jq -r .version | grep "${{ steps.vars.outputs.sha_short }}" || exit 1
- name: Running amsreader-test
run: amsreader-test 10.42.0.11

5
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.vs/
.idea/
[Dd]ebug/
[Rr]elease/
**/__vm/
@@ -18,5 +19,5 @@ platformio-user.ini
node_modules
/gui/dist
/scripts/*dev
/src/KmpCommunicator.cpp
/src/KmpCommunicatorDefs.h
localazy-keys.json
localazy/language

View File

@@ -1,12 +1,15 @@
# AMS Reader
This code is designed to decode data from electric smart meters installed in many countries in Europe these days. The data is presented in a graphical web interface and can also send the data to a MQTT broker which makes it suitable for home automation project. Originally it was only designed to work with Norwegian meters, but has since been adapter to read any IEC-62056-7-5 or IEC-62056-21 compliant meters.
Later development have added Energy usage graph for both day and month, as well as future energy price. The code can run on any ESP8266 or ESP32 hardware which you can read more about in the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki). If you don't have the knowledge to set up a ESP device yourself, or you would like to support our work, please have a look at our shop at [amsleser.no](https://amsleser.no/).
Later development have added Energy usage graph for both day and month, as well as future energy price. The code can run on any ESP8266 or ESP32 hardware which you can read more about in the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki). If you don't have the knowledge to set up a ESP device yourself, or you would like to support our work, please have a look at our shop at [amsleser.no](https://www.amsleser.no/).
<img src="images/dashboard.png">
Go to the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki) for information on how to get your own device! And find the latest prebuilt firmware file at the [release section](https://github.com/UtilitechAS/amsreader-firmware/releases).
## Installing pre-built firmware
If you have a device already running this firmware and you for some reason need to upgrade via USB port, you can use a [this web-based tool](https://www.amsleser.cloud/flasher)
If you are using a development board and want to flash a pre-built firmware manually, get the necessary files from the [release](https://github.com/UtilitechAS/amsreader-firmware/releases) section and visit the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki) and have a look at the [Flashing](https://github.com/UtilitechAS/amsreader-firmware/wiki/flashinghttps://github.com/UtilitechAS/amsreader-firmware/wiki/flashing) section
## Building this project with PlatformIO
To build this project, you need [PlatformIO](https://platformio.org/) installed.

7
custom_partition.csv Normal file
View File

@@ -0,0 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x1D0000,
app1, app, ota_1, 0x1E0000,0x1D0000,
spiffs, data, spiffs, 0x3B0000,0x40000,
coredump, data, coredump,0x3F0000,0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x1D0000
5 app1 app ota_1 0x1E0000 0x1D0000
6 spiffs data spiffs 0x3B0000 0x40000
7 coredump data coredump 0x3F0000 0x10000

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@

View File

@@ -31,3 +31,59 @@ Object with three values Value Object with two values
02 03 09 06 01 00 04 08 00 FF 06 00 0A F5 EC 02 02 0F 01 16 20
51 D7
7E
Received 21.09.24 20:00:10:
(V) 7E A1 8A 41 08 83 13 EB FD E6 E7 00 0F 40 00 00
(V) 00 00 01 12 02 02 09 06 01 01 00 02 81 FF 0A 0B
(V) 41 49 44 4F 4E 5F 56 30 30 30 31 02 02 09 06 00
(V) 00 60 01 00 FF 0A 10 37 33 35 39 39 39 32 38 39
(V) 30 34 39 37 39 39 37 02 02 09 06 00 00 60 01 07
(V) FF 0A 04 36 35 33 34 02 03 09 06 01 00 01 07 00
(V) FF 06 00 00 02 71 02 02 0F 00 16 1B 02 03 09 06
(V) 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16
(V) 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 00
(V) 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF
(V) 06 00 00 01 F0 02 02 0F 00 16 1D 02 03 09 06 01
(V) 00 1F 07 00 FF 10 00 06 02 02 0F FF 16 21 02 03
(V) 09 06 01 00 33 07 00 FF 10 00 05 02 02 0F FF 16
(V) 21 02 03 09 06 01 00 47 07 00 FF 10 00 17 02 02
(V) 0F FF 16 21 02 03 09 06 01 00 20 07 00 FF 12 09
(V) 3F 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00
(V) FF 12 09 36 02 02 0F FF 16 23 02 03 09 06 01 00
(V) 48 07 00 FF 12 09 22 02 02 0F FF 16 23 02 02 09
(V) 06 00 00 01 00 00 FF 09 0C 07 E8 09 15 06 13 00
(V) 00 FF 00 00 00 02 03 09 06 01 00 01 08 00 FF 06
(V) 00 8D 98 13 02 02 0F 01 16 1E 02 03 09 06 01 00
(V) 02 08 00 FF 06 00 00 00 00 02 02 0F 01 16 1E 02
(V) 03 09 06 01 00 03 08 00 FF 06 00 00 29 6E 02 02
(V) 0F 01 16 20 02 03 09 06 01 00 04 08 00 FF 06 00
(V) 13 F2 06 02 02 0F 01 16 20 C8 1D 7E
Received 27.10.24 11:00:13:
(V) 7E A1 8A 41 08 83 13 EB FD E6 E7 00 0F 40 00 00
(V) 00 00 01 12 02 02 09 06 01 01 00 02 81 FF 0A 0B
(V) 41 49 44 4F 4E 5F 56 30 30 30 31 02 02 09 06 00
(V) 00 60 01 00 FF 0A 10 37 33 35 39 39 39 32 38 39
(V) 30 34 39 37 39 39 37 02 02 09 06 00 00 60 01 07
(V) FF 0A 04 36 35 33 34 02 03 09 06 01 00 01 07 00
(V) FF 06 00 00 02 AA 02 02 0F 00 16 1B 02 03 09 06
(V) 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16
(V) 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 00
(V) 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF
(V) 06 00 00 01 CC 02 02 0F 00 16 1D 02 03 09 06 01
(V) 00 1F 07 00 FF 10 00 02 02 02 0F FF 16 21 02 03
(V) 09 06 01 00 33 07 00 FF 10 00 0B 02 02 0F FF 16
(V) 21 02 03 09 06 01 00 47 07 00 FF 10 00 16 02 02
(V) 0F FF 16 21 02 03 09 06 01 00 20 07 00 FF 12 09
(V) 2F 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00
(V) FF 12 09 00 02 02 0F FF 16 23 02 03 09 06 01 00
(V) 48 07 00 FF 12 09 0F 02 02 0F FF 16 23 02 02 09
(V) 06 00 00 01 00 00 FF 09 0C 07 E8 0A 1B 00 0B 00
(V) 00 FF 00 00 00 02 03 09 06 01 00 01 08 00 FF 06
(V) 00 8F 25 6F 02 02 0F 01 16 1E 02 03 09 06 01 00
(V) 02 08 00 FF 06 00 00 00 00 02 02 0F 01 16 1E 02
(V) 03 09 06 01 00 03 08 00 FF 06 00 00 29 8D 02 02
(V) 0F 01 16 20 02 03 09 06 01 00 04 08 00 FF 06 00
(V) 14 44 26 02 02 0F 01 16 20 84 09 7E

14
frames/EGM5G35.txt Normal file
View File

@@ -0,0 +1,14 @@
2F 45 47 4D 35 47 33 35 // DSMR Identifier
0D 0A // CRLF
0D 0A // CRLF
00 // System title, blank
82 // Two-byte length follows
02 30 // 560 bytes
30 // Security tag 0011 0000, 0=Compression off, 0=Unicast, 1=Encryption, 1=Authentication, 0000= Security Suite ID
00 00 00 00 // Frame counter, blank
// Encrypted payload follows (560 - security tag - frame counter = 555 bytes)
30 10 73 D6 59 85 96 B0 2A 8B CC E7 33 AD 19 A3 A5 4A 4D 0E 3A CE F6 33 DB 57 0A 45 9A C3 5B F0 D4 C0 AA 15 B9 93 B0 E8 67 85 EC 22 CA 40 8C D7 1A B5 62 AE DF 0F 48 EA D9 E6 56 FB 84 B3 7B 47 C6 29 12 F1 EE BB 22 88 26 75 5C F8 A2 20 F4 93 F3 64 CA 7C C6 60 32 62 B0 7F F9 7F 71 0A 9F DF AB 61 89 6A 9A 10 B1 DF 94 2C 74 8F 51 B6 09 5C F2 45 6C 38 54 52 FE B6 CB 1F 42 F2 93 DC 57 BE EE 5A 0D F8 1F 53 55 0E 21 EF 46 0A 72 74 CA AB D8 9C A9 05 9E FD 54 5E CC 7B 71 40 E1 22 B2 25 5C D5 63 80 BD 97 6E 54 4B 24 A6 58 1F 79 1F 45 C7 DF C0 83 3B 7F 1E C7 43 B9 26 F6 EB 6E F2 B1 7C 99 3A 91 EA 39 72 E5 FB 72 B1 E1 24 AF 9D 19 8A 6C 50 69 97 90 D3 3D 68 97 57 C2 EC 49 EF 12 1D 11 44 46 70 46 88 88 98 37 6F B0 93 FC D2 3F 37 66 1C C7 F0 93 E3 AF 4D 3C BE 41 8E B9 48 77 A9 92 5B 42 0A 96 E5 34 4D 30 56 90 5A 08 03 CC 41 78 58 79 FD 89 82 E0 46 3D 66 AA 42 85 F4 A8 46 2D 4A E4 81 12 40 68 D6 F5 F9 11 5A 94 51 21 3C 9F 4F F7 FE B3 B5 BF 1E D2 12 CF B9 FB 28 D0 B4 82 9E 11 D8 1C FC E3 36 EC DE 8A 7C 83 76 F7 EB 3D 2C 4A B7 7F 75 91 F1 F5 18 D4 70 6A C9 3E 3D 3F 9E 0F C6 0F A7 E7 20 11 03 E7 75 4E 40 F1 5E BA 86 30 6D C0 28 C4 14 47 DF 6A EF CC FE E4 B6 23 58 2B 9D 4F BF 6A A8 93 84 F4 CD 4D FF 47 B5 C4 81 D2 DA D3 6B C3 F4 1D C4 6E E1 9B 24 30 91 7B 33 57 67 B6 E4 93 FE 8B 5B AE 8A 0D 81 DE A5 C3 3F 82 9A CE E2 BA 6D 50 39 FC 99 E6 7A 7B B4 E9 A9 84 78 AF 2D 4C D2 15 53 6B 98 2B 1F C8 3D 57 9E 49 68 40 0C D3 4A 9E D6 4F 1F 12 50 84 32 AD 62 C6 D3 42 34 46 66 65 56 49 81 F4 BC 8E C6 75 77 11 1F 29 BB 80 20 23 0A 80 D5 31 50 16 89 6F 07 4A 25 BE F0 4B 60 36 87 AE A6 2A 53 79 22 15 A5 2A C7 1D 15 7B 87 DD B5 07 3D 1C 28 C1 B6 51 47 2F 39 DE D9 B9 29 B6 78
21 // End of DSMR (!)
46 45 30 43 // FE0C - Checksum
0D 0A // CRLF

View File

@@ -31,4 +31,183 @@ C9 95 7E // CRC and end tag
06 00 00 00 00 06 00 00 00 00 06 00 00 01 67 06 00 00 03 BF 06 00 00 05 05
06 00 00 24 34 06 00 00 09 45 06 00 00 09 4F 06 00 00 09 3B
09 0C 07 E5 03 17 02 13 00 0A FF 80 00 00 06 01 34 3B 5D 06 00 00 00 00 06 00 00 09 36 06 00 3C 7A 98 DA 15 7E
7E A0 79 01 02 01 10 80 93 E6 E7 00 0F 40 00 00 00 09 0C 07 E1 09 0E 04 15 1F 14 FF 80 00 00 02 0D 09 07 4B 46 4D 5F 30 30 31 09 10 36 39 37 30 36 33 31 34 30 31 37 35 33 39 38 35 09 08 4D 41 33 30 34 48 33 45 06 00 00 04 0C 06 00 00 00 00 06 00 00 00 00 06 00 00 00 4E 06 00 00 07 C1 06 00 00 0C 9E 06 00 00 0D 7E 06 00 00 09 5F 06 00 00 00 00 06 00 00 09 66 87 96 7E
7E A0 79 01 02 01 10 80 93 E6 E7 00 0F 40 00 00 00 09 0C 07 E1 09 0E 04 15 1F 14 FF 80 00 00 02 0D 09 07 4B 46 4D 5F 30 30 31 09 10 36 39 37 30 36 33 31 34 30 31 37 35 33 39 38 35 09 08 4D 41 33 30 34 48 33 45 06 00 00 04 0C 06 00 00 00 00 06 00 00 00 00 06 00 00 00 4E 06 00 00 07 C1 06 00 00 0C 9E 06 00 00 0D 7E 06 00 00 09 5F 06 00 00 00 00 06 00 00 09 66 87 96 7E
01.10.24 19:00 ish:
(D) READY to update (internal clock 16:59:58 UTC, meter clock: 00:00:00, list type 1, est: 1, using clock: 1)
(V) HDLC frame:
(V) 7E A0 78 01 02 01 10 C4 98 E6 E7 00 0F 40 00 00
(V) 00 09 0C 07 E8 0A 01 02 13 00 00 FF 80 00 00 02
(V) 0D 09 07 4B 46 4D 5F 30 30 31 09 10 36 39 37 30
(V) 36 33 31 34 30 37 32 36 32 39 38 36 09 07 4D 41
(V) 33 30 34 48 34 06 00 00 13 C0 06 00 00 00 00 06
(V) 00 00 00 00 06 00 00 01 D5 06 00 00 4C DF 06 00
(V) 00 05 0C 06 00 00 03 9B 06 00 00 09 34 06 00 00
(V) 09 69 06 00 00 09 59 18 EB 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13
(V) 00 00 FF 80 00 00 02 0D 09 07 4B 46 4D 5F 30 30
(V) 31 09 10 36 39 37 30 36 33 31 34 30 37 32 36 32
(V) 39 38 36 09 07 4D 41 33 30 34 48 34 06 00 00 13
(V) C0 06 00 00 00 00 06 00 00 00 00 06 00 00 01 D5
(V) 06 00 00 4C DF 06 00 00 05 0C 06 00 00 03 9B 06
(V) 00 00 09 34 06 00 00 09 69 06 00 00 09 59
(V) DLMS frame:
(V) 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13 00 00 FF
(V) 80 00 00 02 0D 09 07 4B 46 4D 5F 30 30 31 09 10
(V) 36 39 37 30 36 33 31 34 30 37 32 36 32 39 38 36
(V) 09 07 4D 41 33 30 34 48 34 06 00 00 13 C0 06 00
(V) 00 00 00 06 00 00 00 00 06 00 00 01 D5 06 00 00
(V) 4C DF 06 00 00 05 0C 06 00 00 03 9B 06 00 00 09
(V) 34 06 00 00 09 69 06 00 00 09 59
(D) Received valid DLMS at 31
(V) Using application data:
(V) 02 0D 09 07 4B 46 4D 5F 30 30 31 09 10 36 39 37
(V) 30 36 33 31 34 30 37 32 36 32 39 38 36 09 07 4D
(V) 41 33 30 34 48 34 06 00 00 13 C0 06 00 00 00 00
(V) 06 00 00 00 00 06 00 00 01 D5 06 00 00 4C DF 06
(V) 00 00 05 0C 06 00 00 03 9B 06 00 00 09 34 06 00
(V) 00 09 69 06 00 00 09 59
(V) DLMS
(D) READY to update (internal clock 17:00:01 UTC, meter clock: 00:00:00, list type 2, est: 1, using clock: 1)
(V) HDLC frame:
(V) 7E A0 27 01 02 01 10 5A 87 E6 E7 00 0F 40 00 00
(V) 00 09 0C 07 E8 0A 01 02 13 00 02 FF 80 00 00 02
(V) 01 06 00 00 13 C1 56 F5 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13
(V) 00 02 FF 80 00 00 02 01 06 00 00 13 C1
(V) DLMS frame:
(V) 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13 00 02 FF
(V) 80 00 00 02 01 06 00 00 13 C1
(D) Received valid DLMS at 31
(V) Using application data:
(V) 02 01 06 00 00 13 C1
(V) DLMS
(D) READY to update (internal clock 17:00:02 UTC, meter clock: 00:00:00, list type 1, est: 1, using clock: 1)
(V) HDLC frame:
(V) 7E A0 27 01 02 01 10 5A 87 E6 E7 00 0F 40 00 00
(V) 00 09 0C 07 E8 0A 01 02 13 00 04 FF 80 00 00 02
(V) 01 06 00 00 13 BC F9 5A 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13
(V) 00 04 FF 80 00 00 02 01 06 00 00 13 BC
(V) DLMS frame:
(V) 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13 00 04 FF
(V) 80 00 00 02 01 06 00 00 13 BC
(D) Received valid DLMS at 31
(V) Using application data:
(V) 02 01 06 00 00 13 BC
(V) DLMS
(D) READY to update (internal clock 17:00:04 UTC, meter clock: 00:00:00, list type 1, est: 1, using clock: 1)
(V) HDLC frame:
(V) 7E A0 27 01 02 01 10 5A 87 E6 E7 00 0F 40 00 00
(V) 00 09 0C 07 E8 0A 01 02 13 00 06 FF 80 00 00 02
(V) 01 06 00 00 13 BC 42 58 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13
(V) 00 06 FF 80 00 00 02 01 06 00 00 13 BC
(V) DLMS frame:
(V) 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13 00 06 FF
(V) 80 00 00 02 01 06 00 00 13 BC
(D) Received valid DLMS at 31
(V) Using application data:
(V) 02 01 06 00 00 13 BC
(V) DLMS
(D) READY to update (internal clock 17:00:06 UTC, meter clock: 00:00:00, list type 1, est: 1, using clock: 1)
(V) HDLC frame:
(V) 7E A0 27 01 02 01 10 5A 87 E6 E7 00 0F 40 00 00
(V) 00 09 0C 07 E8 0A 01 02 13 00 08 FF 80 00 00 02
(V) 01 06 00 00 13 BB DC 21 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13
(V) 00 08 FF 80 00 00 02 01 06 00 00 13 BB
(V) DLMS frame:
(V) 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13 00 08 FF
(V) 80 00 00 02 01 06 00 00 13 BB
(D) Received valid DLMS at 31
(V) Using application data:
(V) 02 01 06 00 00 13 BB
(V) DLMS
(D) READY to update (internal clock 17:00:08 UTC, meter clock: 00:00:00, list type 1, est: 1, using clock: 1)
(V) HDLC frame:
(V) 7E A0 9A 01 02 01 10 AA A5 E6 E7 00 0F 40 00 00
(V) 00 09 0C 07 E8 0A 01 02 13 00 0A FF 80 00 00 02
(V) 12 09 07 4B 46 4D 5F 30 30 31 09 10 36 39 37 30
(V) 36 33 31 34 30 37 32 36 32 39 38 36 09 07 4D 41
(V) 33 30 34 48 34 06 00 00 13 C0 06 00 00 00 00 06
(V) 00 00 00 00 06 00 00 01 D5 06 00 00 4C DD 06 00
(V) 00 05 0E 06 00 00 03 90 06 00 00 09 34 06 00 00
(V) 09 69 06 00 00 09 59 09 0C 07 E8 0A 01 02 13 00
(V) 0A FF 80 00 00 06 02 8E 4B 5E 06 00 00 00 00 06
(V) 00 97 35 DE 06 00 08 97 2F FA E5 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13
(V) 00 0A FF 80 00 00 02 12 09 07 4B 46 4D 5F 30 30
(V) 31 09 10 36 39 37 30 36 33 31 34 30 37 32 36 32
(V) 39 38 36 09 07 4D 41 33 30 34 48 34 06 00 00 13
(V) C0 06 00 00 00 00 06 00 00 00 00 06 00 00 01 D5
(V) 06 00 00 4C DD 06 00 00 05 0E 06 00 00 03 90 06
(V) 00 00 09 34 06 00 00 09 69 06 00 00 09 59 09 0C
(V) 07 E8 0A 01 02 13 00 0A FF 80 00 00 06 02 8E 4B
(V) 5E 06 00 00 00 00 06 00 97 35 DE 06 00 08 97 2F
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13 00 0A FF
(V) 80 00 00 02 12 09 07 4B 46 4D 5F 30 30 31 09 10
(V) 36 39 37 30 36 33 31 34 30 37 32 36 32 39 38 36
(V) 09 07 4D 41 33 30 34 48 34 06 00 00 13 C0 06 00
(V) 00 00 00 06 00 00 00 00 06 00 00 01 D5 06 00 00
(V) 4C DD 06 00 00 05 0E 06 00 00 03 90 06 00 00 09
(V) 34 06 00 00 09 69 06 00 00 09 59 09 0C 07 E8 0A
(V) 01 02 13 00 0A FF 80 00 00 06 02 8E 4B 5E 06 00
(V) 00 00 00 06 00 97 35 DE 06 00 08 97 2F
(D) Received valid DLMS at 31
(V) Using application data:
(V) 02 12 09 07 4B 46 4D 5F 30 30 31 09 10 36 39 37
(V) 30 36 33 31 34 30 37 32 36 32 39 38 36 09 07 4D
(V) 41 33 30 34 48 34 06 00 00 13 C0 06 00 00 00 00
(V) 06 00 00 00 00 06 00 00 01 D5 06 00 00 4C DD 06
(V) 00 00 05 0E 06 00 00 03 90 06 00 00 09 34 06 00
(V) 00 09 69 06 00 00 09 59 09 0C 07 E8 0A 01 02 13
(V) 00 0A FF 80 00 00 06 02 8E 4B 5E 06 00 00 00 00
(V) 06 00 97 35 DE 06 00 08 97 2F
(V) DLMS
(D) READY to update (internal clock 17:00:12 UTC, meter clock: 17:00:10, list type 3, est: 1, using clock: 0)
(D) Updating data storage using actual data
(D) Clearing hours from 16 to 17
(D) Clearing days from 1 to 1
(D) Day is not happy
(D) - normal
(I) Saving data
(I) Saving energy accounting
(W) Used 1249ms to read HAN port (true)
(V) HDLC frame:
(V) 7E A0 27 01 02 01 10 5A 87 E6 E7 00 0F 40 00 00
(V) 00 09 0C 07 E8 0A 01 02 13 00 0C FF 80 00 00 02
(V) 01 06 00 00 13 BC 15 50 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13
(V) 00 0C FF 80 00 00 02 01 06 00 00 13 BC
(V) DLMS frame:
(V) 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13 00 0C FF
(V) 80 00 00 02 01 06 00 00 13 BC
(D) Received valid DLMS at 31
(V) Using application data:
(V) 02 01 06 00 00 13 BC
(V) DLMS
(D) NOT Ready to update (internal clock 17:00:12 UTC, meter clock: 00:00:00, list type 1, est: 0)
(V) HDLC frame:
(V) 7E A0 27 01 02 01 10 5A 87 E6 E7 00 0F 40 00 00
(V) 00 09 0C 07 E8 0A 01 02 13 00 0E FF 80 00 00 02
(V) 01 06 00 00 13 B7 7D EC 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13
(V) 00 0E FF 80 00 00 02 01 06 00 00 13 B7
(V) DLMS frame:
(V) 0F 40 00 00 00 09 0C 07 E8 0A 01 02 13 00 0E FF
(V) 80 00 00 02 01 06 00 00 13 B7
(D) Received valid DLMS at 31
(V) Using application data:
(V) 02 01 06 00 00 13 B7

File diff suppressed because it is too large Load Diff

682
frames/L&G-E350_Norway.raw Normal file
View File

@@ -0,0 +1,682 @@
*** Remote debug - over telnet - for ESP32 - version 3.0.5
* Host name: ams-e603 IP:10.10.10.62 Mac address:D8:3B:DA:C4:03:E6
* Free Heap RAM: 87952
* ESP SDK version: 4.4.5.230722
******************************************************
* Commands:
? or help -> display these help of commands
q -> quit (close this connection)
m -> display memory available
v -> set debug level to verbose
d -> set debug level to debug
i -> set debug level to info
w -> set debug level to warning
e -> set debug level to errors
s -> set debug silence on/off
l -> show debug level
t -> show time (millis)
profiler:
p -> show time between actual and last message (in millis)
p min -> show only if time is this minimal
P time -> set debug level to profiler
c -> show colors
filter:
filter <string> -> show only debugs with this
nofilter -> disable the filter
* Please type the command and press enter to execute.(? or h for this help)
***
(V) HDLC frame:
(V) 7E A1 1E 41 08 83 13 EE EE E6 E7 00 0F 40 00 00
(V) 00 00 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B
(V) 45 4D 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00
(V) 00 60 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00
(V) 00 00 00 00 00 00 00 02 02 09 06 00 00 60 01 07
(V) FF 0A 04 5A 46 46 31 02 03 09 06 01 00 01 07 00
(V) FF 06 00 00 24 F4 02 02 0F 00 16 1B 02 03 09 06
(V) 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16
(V) 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 31
(V) 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF
(V) 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01
(V) 00 1F 07 00 FF 10 0C 6C 02 02 0F FE 16 21 02 03
(V) 09 06 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16
(V) 21 02 03 09 06 01 00 47 07 00 FF 10 0D 68 02 02
(V) 0F FE 16 21 02 03 09 06 01 00 20 07 00 FF 12 09
(V) 2E 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00
(V) FF 12 00 00 02 02 0F FF 16 23 02 03 09 06 01 00
(V) 48 07 00 FF 12 09 10 02 02 0F FF 16 23 2F AF 7E
(V)
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 0D 02 02 09 06 01
(V) 01 00 02 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31
(V) 5F 31 02 02 09 06 00 00 60 01 00 FF 0A 10 31 33
(V) 39 34 33 35 39 37 00 00 00 00 00 00 00 00 02 02
(V) 09 06 00 00 60 01 07 FF 0A 04 5A 46 46 31 02 03
(V) 09 06 01 00 01 07 00 FF 06 00 00 24 F4 02 02 0F
(V) 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00
(V) 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07
(V) 00 FF 06 00 00 00 31 02 02 0F 00 16 1D 02 03 09
(V) 06 01 00 04 07 00 FF 06 00 00 00 00 02 02 0F 00
(V) 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 0C 6C 02
(V) 02 0F FE 16 21 02 03 09 06 01 00 33 07 00 FF 10
(V) 00 00 02 02 0F FE 16 21 02 03 09 06 01 00 47 07
(V) 00 FF 10 0D 68 02 02 0F FE 16 21 02 03 09 06 01
(V) 00 20 07 00 FF 12 09 2E 02 02 0F FF 16 23 02 03
(V) 09 06 01 00 34 07 00 FF 12 00 00 02 02 0F FF 16
(V) 23 02 03 09 06 01 00 48 07 00 FF 12 09 10 02 02
(V) 0F FF 16 23
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 0D 02 02 09 06 01 01 00 02
(V) 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31 5F 31 02
(V) 02 09 06 00 00 60 01 00 FF 0A 10 31 33 39 34 33
(V) 35 39 37 00 00 00 00 00 00 00 00 02 02 09 06 00
(V) 00 60 01 07 FF 0A 04 5A 46 46 31 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 F4 02 02 0F 00 16 1B
(V) 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02
(V) 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06
(V) 00 00 00 31 02 02 0F 00 16 1D 02 03 09 06 01 00
(V) 04 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02
(V) 03 09 06 01 00 1F 07 00 FF 10 0C 6C 02 02 0F FE
(V) 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 00 02
(V) 02 0F FE 16 21 02 03 09 06 01 00 47 07 00 FF 10
(V) 0D 68 02 02 0F FE 16 21 02 03 09 06 01 00 20 07
(V) 00 FF 12 09 2E 02 02 0F FF 16 23 02 03 09 06 01
(V) 00 34 07 00 FF 12 00 00 02 02 0F FF 16 23 02 03
(V) 09 06 01 00 48 07 00 FF 12 09 10 02 02 0F FF 16
(V) 23
(D) Received valid DLMS at 18 +267
(V) Using application data:
(V) 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B 45 4D
(V) 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00 00 60
(V) 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00 00 00
(V) 00 00 00 00 00 02 02 09 06 00 00 60 01 07 FF 0A
(V) 04 5A 46 46 31 02 03 09 06 01 00 01 07 00 FF 06
(V) 00 00 24 F4 02 02 0F 00 16 1B 02 03 09 06 01 00
(V) 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02
(V) 03 09 06 01 00 03 07 00 FF 06 00 00 00 31 02 02
(V) 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00
(V) 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 1F
(V) 07 00 FF 10 0C 6C 02 02 0F FE 16 21 02 03 09 06
(V) 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16 21 02
(V) 03 09 06 01 00 47 07 00 FF 10 0D 68 02 02 0F FE
(V) 16 21 02 03 09 06 01 00 20 07 00 FF 12 09 2E 02
(V) 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12
(V) 00 00 02 02 0F FF 16 23 02 03 09 06 01 00 48 07
(V) 00 FF 12 09 10 02 02 0F FF 16 23
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:06 UTC, meter clock: 00:00:00, list type 2, est: 1)
(V) HDLC frame:
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
(V) 00 24 F4 02 02 0F 00 16 1B A7 7F 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 F4 02 02 0F 00 16 1B
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
(V) 00 FF 06 00 00 24 F4 02 02 0F 00 16 1B
(D) Received valid DLMS at 18 +23
(V) Using application data:
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
(V) F4 02 02 0F 00 16 1B
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:07 UTC, meter clock: 00:00:00, list type 1, est: 1)
(V) HDLC frame:
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
(V) 00 24 F4 02 02 0F 00 16 1B A7 7F 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 F4 02 02 0F 00 16 1B
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
(V) 00 FF 06 00 00 24 F4 02 02 0F 00 16 1B
(D) Received valid DLMS at 18 +23
(V) Using application data:
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
(V) F4 02 02 0F 00 16 1B
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:10 UTC, meter clock: 00:00:00, list type 1, est: 1)
(V) HDLC frame:
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
(V) 00 24 E0 02 02 0F 00 16 1B 18 A5 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 E0 02 02 0F 00 16 1B
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
(V) 00 FF 06 00 00 24 E0 02 02 0F 00 16 1B
(D) Received valid DLMS at 18 +23
(V) Using application data:
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
(V) E0 02 02 0F 00 16 1B
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:12 UTC, meter clock: 00:00:00, list type 1, est: 1)
(V) HDLC frame:
(V) 7E A1 1E 41 08 83 13 EE EE E6 E7 00 0F 40 00 00
(V) 00 00 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B
(V) 45 4D 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00
(V) 00 60 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00
(V) 00 00 00 00 00 00 00 02 02 09 06 00 00 60 01 07
(V) FF 0A 04 00 00 00 00 02 03 09 06 01 00 01 07 00
(V) FF 06 00 00 24 E0 02 02 0F 00 16 1B 02 03 09 06
(V) 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16
(V) 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 28
(V) 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF
(V) 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01
(V) 00 1F 07 00 FF 10 0C 68 02 02 0F FE 16 21 02 03
(V) 09 06 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16
(V) 21 02 03 09 06 01 00 47 07 00 FF 10 0D 5D 02 02
(V) 0F FE 16 21 02 03 09 06 01 00 20 07 00 FF 12 09
(V) 2E 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00
(V) FF 12 00 00 02 02 0F FF 16 23 02 03 09 06 01 00
(V) 48 07 00 FF 12 09 10 02 02 0F FF 16 23 AE 9D 7E
(V)
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 0D 02 02 09 06 01
(V) 01 00 02 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31
(V) 5F 31 02 02 09 06 00 00 60 01 00 FF 0A 10 31 33
(V) 39 34 33 35 39 37 00 00 00 00 00 00 00 00 02 02
(V) 09 06 00 00 60 01 07 FF 0A 04 00 00 00 00 02 03
(V) 09 06 01 00 01 07 00 FF 06 00 00 24 E0 02 02 0F
(V) 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00
(V) 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07
(V) 00 FF 06 00 00 00 28 02 02 0F 00 16 1D 02 03 09
(V) 06 01 00 04 07 00 FF 06 00 00 00 00 02 02 0F 00
(V) 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 0C 68 02
(V) 02 0F FE 16 21 02 03 09 06 01 00 33 07 00 FF 10
(V) 00 00 02 02 0F FE 16 21 02 03 09 06 01 00 47 07
(V) 00 FF 10 0D 5D 02 02 0F FE 16 21 02 03 09 06 01
(V) 00 20 07 00 FF 12 09 2E 02 02 0F FF 16 23 02 03
(V) 09 06 01 00 34 07 00 FF 12 00 00 02 02 0F FF 16
(V) 23 02 03 09 06 01 00 48 07 00 FF 12 09 10 02 02
(V) 0F FF 16 23
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 0D 02 02 09 06 01 01 00 02
(V) 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31 5F 31 02
(V) 02 09 06 00 00 60 01 00 FF 0A 10 31 33 39 34 33
(V) 35 39 37 00 00 00 00 00 00 00 00 02 02 09 06 00
(V) 00 60 01 07 FF 0A 04 00 00 00 00 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 E0 02 02 0F 00 16 1B
(V) 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02
(V) 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06
(V) 00 00 00 28 02 02 0F 00 16 1D 02 03 09 06 01 00
(V) 04 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02
(V) 03 09 06 01 00 1F 07 00 FF 10 0C 68 02 02 0F FE
(V) 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 00 02
(V) 02 0F FE 16 21 02 03 09 06 01 00 47 07 00 FF 10
(V) 0D 5D 02 02 0F FE 16 21 02 03 09 06 01 00 20 07
(V) 00 FF 12 09 2E 02 02 0F FF 16 23 02 03 09 06 01
(V) 00 34 07 00 FF 12 00 00 02 02 0F FF 16 23 02 03
(V) 09 06 01 00 48 07 00 FF 12 09 10 02 02 0F FF 16
(V) 23
(D) Received valid DLMS at 18 +267
(V) Using application data:
(V) 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B 45 4D
(V) 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00 00 60
(V) 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00 00 00
(V) 00 00 00 00 00 02 02 09 06 00 00 60 01 07 FF 0A
(V) 04 00 00 00 00 02 03 09 06 01 00 01 07 00 FF 06
(V) 00 00 24 E0 02 02 0F 00 16 1B 02 03 09 06 01 00
(V) 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02
(V) 03 09 06 01 00 03 07 00 FF 06 00 00 00 28 02 02
(V) 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00
(V) 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 1F
(V) 07 00 FF 10 0C 68 02 02 0F FE 16 21 02 03 09 06
(V) 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16 21 02
(V) 03 09 06 01 00 47 07 00 FF 10 0D 5D 02 02 0F FE
(V) 16 21 02 03 09 06 01 00 20 07 00 FF 12 09 2E 02
(V) 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12
(V) 00 00 02 02 0F FF 16 23 02 03 09 06 01 00 48 07
(V) 00 FF 12 09 10 02 02 0F FF 16 23
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:16 UTC, meter clock: 00:00:00, list type 2, est: 1)
(V) HDLC frame:
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
(V) 00 24 E0 02 02 0F 00 16 1B 18 A5 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 E0 02 02 0F 00 16 1B
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
(V) 00 FF 06 00 00 24 E0 02 02 0F 00 16 1B
(D) Received valid DLMS at 18 +23
(V) Using application data:
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
(V) E0 02 02 0F 00 16 1B
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:17 UTC, meter clock: 00:00:00, list type 1, est: 1)
(V) HDLC frame:
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
(V) 00 24 A4 02 02 0F 00 16 1B 68 0D 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
(V) 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
(D) Received valid DLMS at 18 +23
(V) Using application data:
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
(V) A4 02 02 0F 00 16 1B
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:20 UTC, meter clock: 00:00:00, list type 1, est: 1)
(V) HDLC frame:
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
(V) 00 24 A4 02 02 0F 00 16 1B 68 0D 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
(V) 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
(D) Received valid DLMS at 18 +23
(V) Using application data:
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
(V) A4 02 02 0F 00 16 1B
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:22 UTC, meter clock: 00:00:00, list type 1, est: 1)
(V) HDLC frame:
(V) 7E A1 1E 41 08 83 13 EE EE E6 E7 00 0F 40 00 00
(V) 00 00 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B
(V) 45 4D 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00
(V) 00 60 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00
(V) 00 00 00 00 00 00 00 02 02 09 06 00 00 60 01 07
(V) FF 0A 04 5A 46 46 31 02 03 09 06 01 00 01 07 00
(V) FF 06 00 00 24 A4 02 02 0F 00 16 1B 02 03 09 06
(V) 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16
(V) 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 31
(V) 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF
(V) 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01
(V) 00 1F 07 00 FF 10 0C 6E 02 02 0F FE 16 21 02 03
(V) 09 06 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16
(V) 21 02 03 09 06 01 00 47 07 00 FF 10 0D 5C 02 02
(V) 0F FE 16 21 02 03 09 06 01 00 20 07 00 FF 12 09
(V) 38 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00
(V) FF 12 00 00 02 02 0F FF 16 23 02 03 09 06 01 00
(V) 48 07 00 FF 12 09 10 02 02 0F FF 16 23 4A DF 7E
(V)
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 0D 02 02 09 06 01
(V) 01 00 02 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31
(V) 5F 31 02 02 09 06 00 00 60 01 00 FF 0A 10 31 33
(V) 39 34 33 35 39 37 00 00 00 00 00 00 00 00 02 02
(V) 09 06 00 00 60 01 07 FF 0A 04 5A 46 46 31 02 03
(V) 09 06 01 00 01 07 00 FF 06 00 00 24 A4 02 02 0F
(V) 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00
(V) 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07
(V) 00 FF 06 00 00 00 31 02 02 0F 00 16 1D 02 03 09
(V) 06 01 00 04 07 00 FF 06 00 00 00 00 02 02 0F 00
(V) 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 0C 6E 02
(V) 02 0F FE 16 21 02 03 09 06 01 00 33 07 00 FF 10
(V) 00 00 02 02 0F FE 16 21 02 03 09 06 01 00 47 07
(V) 00 FF 10 0D 5C 02 02 0F FE 16 21 02 03 09 06 01
(V) 00 20 07 00 FF 12 09 38 02 02 0F FF 16 23 02 03
(V) 09 06 01 00 34 07 00 FF 12 00 00 02 02 0F FF 16
(V) 23 02 03 09 06 01 00 48 07 00 FF 12 09 10 02 02
(V) 0F FF 16 23
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 0D 02 02 09 06 01 01 00 02
(V) 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31 5F 31 02
(V) 02 09 06 00 00 60 01 00 FF 0A 10 31 33 39 34 33
(V) 35 39 37 00 00 00 00 00 00 00 00 02 02 09 06 00
(V) 00 60 01 07 FF 0A 04 5A 46 46 31 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
(V) 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02
(V) 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06
(V) 00 00 00 31 02 02 0F 00 16 1D 02 03 09 06 01 00
(V) 04 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02
(V) 03 09 06 01 00 1F 07 00 FF 10 0C 6E 02 02 0F FE
(V) 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 00 02
(V) 02 0F FE 16 21 02 03 09 06 01 00 47 07 00 FF 10
(V) 0D 5C 02 02 0F FE 16 21 02 03 09 06 01 00 20 07
(V) 00 FF 12 09 38 02 02 0F FF 16 23 02 03 09 06 01
(V) 00 34 07 00 FF 12 00 00 02 02 0F FF 16 23 02 03
(V) 09 06 01 00 48 07 00 FF 12 09 10 02 02 0F FF 16
(V) 23
(D) Received valid DLMS at 18 +267
(V) Using application data:
(V) 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B 45 4D
(V) 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00 00 60
(V) 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00 00 00
(V) 00 00 00 00 00 02 02 09 06 00 00 60 01 07 FF 0A
(V) 04 5A 46 46 31 02 03 09 06 01 00 01 07 00 FF 06
(V) 00 00 24 A4 02 02 0F 00 16 1B 02 03 09 06 01 00
(V) 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02
(V) 03 09 06 01 00 03 07 00 FF 06 00 00 00 31 02 02
(V) 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00
(V) 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 1F
(V) 07 00 FF 10 0C 6E 02 02 0F FE 16 21 02 03 09 06
(V) 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16 21 02
(V) 03 09 06 01 00 47 07 00 FF 10 0D 5C 02 02 0F FE
(V) 16 21 02 03 09 06 01 00 20 07 00 FF 12 09 38 02
(V) 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12
(V) 00 00 02 02 0F FF 16 23 02 03 09 06 01 00 48 07
(V) 00 FF 12 09 10 02 02 0F FF 16 23
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:26 UTC, meter clock: 00:00:00, list type 2, est: 1)
(V) HDLC frame:
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
(V) 00 24 B8 02 02 0F 00 16 1B 3B 09 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
(V) 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
(D) Received valid DLMS at 18 +23
(V) Using application data:
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
(V) B8 02 02 0F 00 16 1B
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:27 UTC, meter clock: 00:00:00, list type 1, est: 1)
(V) HDLC frame:
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
(V) 00 24 B8 02 02 0F 00 16 1B 3B 09 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
(V) 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
(D) Received valid DLMS at 18 +23
(V) Using application data:
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
(V) B8 02 02 0F 00 16 1B
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:29 UTC, meter clock: 00:00:00, list type 1, est: 1)
(V) HDLC frame:
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
(V) 00 24 B8 02 02 0F 00 16 1B 3B 09 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
(V) 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
(D) Received valid DLMS at 18 +23
(V) Using application data:
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
(V) B8 02 02 0F 00 16 1B
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:32 UTC, meter clock: 00:00:00, list type 1, est: 1)
q(V) HDLC frame:
(V) 7E A1 1E 41 08 83 13 EE EE E6 E7 00 0F 40 00 00
(V) 00 00 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B
(V) 45 4D 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00
(V) 00 60 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00
(V) 00 00 00 00 00 00 00 02 02 09 06 00 00 60 01 07
(V) FF 0A 04 5A 46 46 31 02 03 09 06 01 00 01 07 00
(V) FF 06 00 00 24 A4 02 02 0F 00 16 1B 02 03 09 06
(V) 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16
(V) 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 46
(V) 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF
(V) 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01
(V) 00 1F 07 00 FF 10 0C 67 02 02 0F FE 16 21 02 03
(V) 09 06 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16
(V) 21 02 03 09 06 01 00 47 07 00 FF 10 0D 5D 02 02
(V) 0F FE 16 21 02 03 09 06 01 00 20 07 00 FF 12 09
(V) 2E 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00
(V) FF 12 00 00 02 02 0F FF 16 23 02 03 09 06 01 00
(V) 48 07 00 FF 12 09 06 02 02 0F FF 16 23 3A 49 7E
(V)
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 0D 02 02 09 06 01
(V) 01 00 02 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31
(V) 5F 31 02 02 09 06 00 00 60 01 00 FF 0A 10 31 33
(V) 39 34 33 35 39 37 00 00 00 00 00 00 00 00 02 02
(V) 09 06 00 00 60 01 07 FF 0A 04 5A 46 46 31 02 03
(V) 09 06 01 00 01 07 00 FF 06 00 00 24 A4 02 02 0F
(V) 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00
(V) 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07
(V) 00 FF 06 00 00 00 46 02 02 0F 00 16 1D 02 03 09
(V) 06 01 00 04 07 00 FF 06 00 00 00 00 02 02 0F 00
(V) 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 0C 67 02
(V) 02 0F FE 16 21 02 03 09 06 01 00 33 07 00 FF 10
(V) 00 00 02 02 0F FE 16 21 02 03 09 06 01 00 47 07
(V) 00 FF 10 0D 5D 02 02 0F FE 16 21 02 03 09 06 01
(V) 00 20 07 00 FF 12 09 2E 02 02 0F FF 16 23 02 03
(V) 09 06 01 00 34 07 00 FF 12 00 00 02 02 0F FF 16
(V) 23 02 03 09 06 01 00 48 07 00 FF 12 09 06 02 02
(V) 0F FF 16 23
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 0D 02 02 09 06 01 01 00 02
(V) 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31 5F 31 02
(V) 02 09 06 00 00 60 01 00 FF 0A 10 31 33 39 34 33
(V) 35 39 37 00 00 00 00 00 00 00 00 02 02 09 06 00
(V) 00 60 01 07 FF 0A 04 5A 46 46 31 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
(V) 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02
(V) 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06
(V) 00 00 00 46 02 02 0F 00 16 1D 02 03 09 06 01 00
(V) 04 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02
(V) 03 09 06 01 00 1F 07 00 FF 10 0C 67 02 02 0F FE
(V) 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 00 02
(V) 02 0F FE 16 21 02 03 09 06 01 00 47 07 00 FF 10
(V) 0D 5D 02 02 0F FE 16 21 02 03 09 06 01 00 20 07
(V) 00 FF 12 09 2E 02 02 0F FF 16 23 02 03 09 06 01
(V) 00 34 07 00 FF 12 00 00 02 02 0F FF 16 23 02 03
(V) 09 06 01 00 48 07 00 FF 12 09 06 02 02 0F FF 16
(V) 23
(D) Received valid DLMS at 18 +267
(V) Using application data:
(V) 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B 45 4D
(V) 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00 00 60
(V) 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00 00 00
(V) 00 00 00 00 00 02 02 09 06 00 00 60 01 07 FF 0A
(V) 04 5A 46 46 31 02 03 09 06 01 00 01 07 00 FF 06
(V) 00 00 24 A4 02 02 0F 00 16 1B 02 03 09 06 01 00
(V) 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02
(V) 03 09 06 01 00 03 07 00 FF 06 00 00 00 46 02 02
(V) 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00
(V) 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 1F
(V) 07 00 FF 10 0C 67 02 02 0F FE 16 21 02 03 09 06
(V) 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16 21 02
(V) 03 09 06 01 00 47 07 00 FF 10 0D 5D 02 02 0F FE
(V) 16 21 02 03 09 06 01 00 20 07 00 FF 12 09 2E 02
(V) 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12
(V) 00 00 02 02 0F FF 16 23 02 03 09 06 01 00 48 07
(V) 00 FF 12 09 06 02 02 0F FF 16 23
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:36 UTC, meter clock: 00:00:00, list type 2, est: 1)
(V) HDLC frame:
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
(V) 00 24 A4 02 02 0F 00 16 1B 68 0D 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
(V) 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
(D) Received valid DLMS at 18 +23
(V) Using application data:
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
(V) A4 02 02 0F 00 16 1B
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:37 UTC, meter clock: 00:00:00, list type 1, est: 1)
(V) HDLC frame:
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
(V) 00 24 A4 02 02 0F 00 16 1B 68 0D 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
(V) 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
(D) Received valid DLMS at 18 +23
(V) Using application data:
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
(V) A4 02 02 0F 00 16 1B
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:39 UTC, meter clock: 00:00:00, list type 1, est: 1)
(V) HDLC frame:
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
(V) 00 24 D5 02 02 0F 00 16 1B F1 83 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 D5 02 02 0F 00 16 1B
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
(V) 00 FF 06 00 00 24 D5 02 02 0F 00 16 1B
(D) Received valid DLMS at 18 +23
(V) Using application data:
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
(V) D5 02 02 0F 00 16 1B
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:42 UTC, meter clock: 00:00:00, list type 1, est: 1)
(V) HDLC frame:
(V) 7E A1 1E 41 08 83 13 EE EE E6 E7 00 0F 40 00 00
(V) 00 00 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B
(V) 45 4D 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00
(V) 00 60 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00
(V) 00 00 00 00 00 00 00 02 02 09 06 00 00 60 01 07
(V) FF 0A 04 5A 46 46 31 02 03 09 06 01 00 01 07 00
(V) FF 06 00 00 24 D5 02 02 0F 00 16 1B 02 03 09 06
(V) 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16
(V) 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 3C
(V) 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF
(V) 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01
(V) 00 1F 07 00 FF 10 0C 77 02 02 0F FE 16 21 02 03
(V) 09 06 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16
(V) 21 02 03 09 06 01 00 47 07 00 FF 10 0D 5F 02 02
(V) 0F FE 16 21 02 03 09 06 01 00 20 07 00 FF 12 09
(V) 2E 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00
(V) FF 12 00 00 02 02 0F FF 16 23 02 03 09 06 01 00
(V) 48 07 00 FF 12 09 06 02 02 0F FF 16 23 4A 9D 7E
(V)
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 0D 02 02 09 06 01
(V) 01 00 02 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31
(V) 5F 31 02 02 09 06 00 00 60 01 00 FF 0A 10 31 33
(V) 39 34 33 35 39 37 00 00 00 00 00 00 00 00 02 02
(V) 09 06 00 00 60 01 07 FF 0A 04 5A 46 46 31 02 03
(V) 09 06 01 00 01 07 00 FF 06 00 00 24 D5 02 02 0F
(V) 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00
(V) 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07
(V) 00 FF 06 00 00 00 3C 02 02 0F 00 16 1D 02 03 09
(V) 06 01 00 04 07 00 FF 06 00 00 00 00 02 02 0F 00
(V) 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 0C 77 02
(V) 02 0F FE 16 21 02 03 09 06 01 00 33 07 00 FF 10
(V) 00 00 02 02 0F FE 16 21 02 03 09 06 01 00 47 07
(V) 00 FF 10 0D 5F 02 02 0F FE 16 21 02 03 09 06 01
(V) 00 20 07 00 FF 12 09 2E 02 02 0F FF 16 23 02 03
(V) 09 06 01 00 34 07 00 FF 12 00 00 02 02 0F FF 16
(V) 23 02 03 09 06 01 00 48 07 00 FF 12 09 06 02 02
(V) 0F FF 16 23
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 0D 02 02 09 06 01 01 00 02
(V) 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31 5F 31 02
(V) 02 09 06 00 00 60 01 00 FF 0A 10 31 33 39 34 33
(V) 35 39 37 00 00 00 00 00 00 00 00 02 02 09 06 00
(V) 00 60 01 07 FF 0A 04 5A 46 46 31 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 D5 02 02 0F 00 16 1B
(V) 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02
(V) 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06
(V) 00 00 00 3C 02 02 0F 00 16 1D 02 03 09 06 01 00
(V) 04 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02
(V) 03 09 06 01 00 1F 07 00 FF 10 0C 77 02 02 0F FE
(V) 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 00 02
(V) 02 0F FE 16 21 02 03 09 06 01 00 47 07 00 FF 10
(V) 0D 5F 02 02 0F FE 16 21 02 03 09 06 01 00 20 07
(V) 00 FF 12 09 2E 02 02 0F FF 16 23 02 03 09 06 01
(V) 00 34 07 00 FF 12 00 00 02 02 0F FF 16 23 02 03
(V) 09 06 01 00 48 07 00 FF 12 09 06 02 02 0F FF 16
(V) 23
(D) Received valid DLMS at 18 +267
(V) Using application data:
(V) 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B 45 4D
(V) 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00 00 60
(V) 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00 00 00
(V) 00 00 00 00 00 02 02 09 06 00 00 60 01 07 FF 0A
(V) 04 5A 46 46 31 02 03 09 06 01 00 01 07 00 FF 06
(V) 00 00 24 D5 02 02 0F 00 16 1B 02 03 09 06 01 00
(V) 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02
(V) 03 09 06 01 00 03 07 00 FF 06 00 00 00 3C 02 02
(V) 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00
(V) 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 1F
(V) 07 00 FF 10 0C 77 02 02 0F FE 16 21 02 03 09 06
(V) 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16 21 02
(V) 03 09 06 01 00 47 07 00 FF 10 0D 5F 02 02 0F FE
(V) 16 21 02 03 09 06 01 00 20 07 00 FF 12 09 2E 02
(V) 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12
(V) 00 00 02 02 0F FF 16 23 02 03 09 06 01 00 48 07
(V) 00 FF 12 09 06 02 02 0F FF 16 23
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:46 UTC, meter clock: 00:00:00, list type 2, est: 1)
qq(V) HDLC frame:
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
(V) 00 24 D5 02 02 0F 00 16 1B F1 83 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 D5 02 02 0F 00 16 1B
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
(V) 00 FF 06 00 00 24 D5 02 02 0F 00 16 1B
(D) Received valid DLMS at 18 +23
(V) Using application data:
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
(V) D5 02 02 0F 00 16 1B
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:47 UTC, meter clock: 00:00:00, list type 1, est: 1)
q
* Debug: Command received: qqqqq
(V) HDLC frame:
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
(V) 00 24 B8 02 02 0F 00 16 1B 3B 09 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
(V) 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
(D) Received valid DLMS at 18 +23
(V) Using application data:
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
(V) B8 02 02 0F 00 16 1B
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:49 UTC, meter clock: 00:00:00, list type 1, est: 1)
(V) HDLC frame:
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
(V) 00 24 B8 02 02 0F 00 16 1B 3B 09 7E
(V) LLC frame:
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
(V) 00 01 07 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
(V)
(V) DLMS frame:
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
(V) 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
(D) Received valid DLMS at 18 +23
(V) Using application data:
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
(V) B8 02 02 0F 00 16 1B
(V) DLMS
(D) NOT Ready to update (internal clock 12:32:51 UTC, meter clock: 00:00:00, list type 1, est: 1)
q
* Debug: Command received: q
* Closing client connection ...

1
frames/dsmr.raw Normal file
View File

@@ -0,0 +1 @@
2F454C4C355C3235333833333633355F410D0A0D0A302D303A312E302E302832343132313232303133343857290D0A312D303A312E382E302830303034373532322E3237382A6B5768290D0A312D303A322E382E302830303030303030302E3030312A6B5768290D0A312D303A332E382E302830303030303035392E3033392A6B76617268290D0A312D303A342E382E302830303031323537302E3937362A6B76617268290D0A312D303A312E372E3028303030312E3233312A6B57290D0A312D303A322E372E3028303030302E3030302A6B57290D0A312D303A332E372E3028303030302E3030302A6B766172290D0A312D303A342E372E3028303030302E3435312A6B766172290D0A312D303A32312E372E3028303030302E3337362A6B57290D0A312D303A34312E372E3028303030302E3832342A6B57290D0A312D303A36312E372E3028303030302E3033302A6B57290D0A312D303A32322E372E3028303030302E3030302A6B57290D0A312D303A34322E372E3028303030302E3030302A6B57290D0A312D303A36322E372E3028303030302E3030302A6B57290D0A312D303A32332E372E3028303030302E3030302A6B766172290D0A312D303A34332E372E3028303030302E3030302A6B766172290D0A312D303A36332E372E3028303030302E3030302A6B766172290D0A312D303A32342E372E3028303030302E3037322A6B766172290D0A312D303A34342E372E3028303030302E3334392A6B766172290D0A312D303A36342E372E3028303030302E3032392A6B766172290D0A312D303A33322E372E30283233322E382A56290D0A312D303A35322E372E30283233332E352A56290D0A312D303A37322E372E30283233382E352A56290D0A312D303A33312E372E30283030312E382A41290D0A312D303A35312E372E30283030342E302A41290D0A312D303A37312E372E30283030302E312A41290D0A21343736300D0A

48
frames/iskra_croatia.txt Normal file
View File

@@ -0,0 +1,48 @@
They actually use multiple frames, so this is a "fake" frame combining the two into one, but without checksum fields.
7E
A0 BD
CF 02 23 03 00 00
E6 E7 00
0F 00 03 46 3B
0C 07 E9 0C 13 05 17 37 28 00 FF C4 00
02 21
09 08 39 32 30 32 39 36 39 31
09 04 17 37 28 00
09 05 07 E9 0C 13 05
06 00 6C 28 5A
06 00 4B 76 1A
06 00 20 B2 40
06 00 58 68 AA
06 00 57 A1 62
06 00 00 C7 48
06 00 17 EE D7
06 00 12 F5 5C
06 00 00 D9 6A
06 00 15 36 84
06 00 00 01 7E
06 00 00 00 00
12 03 79
06 00 00 00 7F
06 00 00 00 BD
06 00 00 00 41
06 00 00 00 00
06 00 00 00 00
06 00 00 00 00
12 09 54
12 09 35
12 09 49
12 00 37
12 00 59
12 00 4D
06 00 00 43 62
01 01
12 24 B8
01 01
12 24 B8
01 01
12 24 B8
03 01
00 00 7E

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 199 KiB

View File

@@ -13,10 +13,8 @@
#define EEPROM_CHECK_SUM 104 // Used to check if config is stored. Change if structure changes
#define EEPROM_CLEARED_INDICATOR 0xFC
#define EEPROM_CONFIG_ADDRESS 0
#define EEPROM_TEMP_CONFIG_ADDRESS 2048
#define CONFIG_SYSTEM_START 8
#define CONFIG_UPGRADE_INFO_START 16
#define CONFIG_NETWORK_START 40
#define CONFIG_METER_START 296
#define CONFIG_GPIO_START 368
@@ -30,6 +28,8 @@
#define CONFIG_HA_START 1552
#define CONFIG_UI_START 1720
#define CONFIG_CLOUD_START 1742
#define CONFIG_UPGRADE_INFO_START 1934
#define CONFIG_ZC_START 2000
#define CONFIG_METER_START_103 32
#define CONFIG_UPGRADE_INFO_START_103 216
@@ -50,6 +50,22 @@
#define LED_BEHAVIOUR_ERROR_ONLY 3
#define LED_BEHAVIOUR_OFF 9
#define FIRMWARE_CHANNEL_STABLE 0
#define FIRMWARE_CHANNEL_EARLY 1
#define FIRMWARE_CHANNEL_RC 2
#define FIRMWARE_CHANNEL_SNAPSHOT 3
#define REBOOT_CAUSE_WEB_SYSINFO_JSON 1
#define REBOOT_CAUSE_WEB_SAVE 2
#define REBOOT_CAUSE_WEB_REBOOT 3
#define REBOOT_CAUSE_WEB_FACTORY_RESET 4
#define REBOOT_CAUSE_BTN_FACTORY_RESET 5
#define REBOOT_CAUSE_REPARTITION 6
#define REBOOT_CAUSE_CONFIG_FILE_UPDATE 7
#define REBOOT_CAUSE_FIRMWARE_UPDATE 8
#define REBOOT_CAUSE_MQTT_DISCONNECTED 9
#define REBOOT_CAUSE_SMART_CONFIG 10
struct ResetDataContainer {
uint8_t cause;
uint8_t last_cause;
@@ -63,6 +79,7 @@ struct SystemConfig {
uint8_t dataCollectionConsent; // 0 = unknown, 1 = accepted, 2 = declined
char country[3];
uint8_t energyspeedometer;
uint8_t firmwareChannel;
}; // 9
struct NetworkConfig {
@@ -92,7 +109,13 @@ struct MqttConfig {
char password[256];
uint8_t payloadFormat;
bool ssl;
}; // 676
uint8_t magic;
bool stateUpdate;
uint16_t stateUpdateInterval;
uint16_t timeout;
uint8_t keepalive;
uint8_t rebootMinutes;
}; // 684
struct WebConfig {
uint8_t security;
@@ -152,7 +175,8 @@ struct GpioConfig {
uint16_t vccResistorVcc;
uint8_t ledDisablePin;
uint8_t ledBehaviour;
}; // 21
uint8_t powersaving;
}; // 22
struct GpioConfig103 {
uint8_t hanPin;
@@ -201,21 +225,18 @@ struct PriceServiceConfig {
char entsoeToken[37];
char area[17];
char currency[4];
uint32_t unused1;
bool enabled;
uint8_t resolutionInMinutes;
uint16_t unused2;
}; // 64
uint16_t unused3;
bool enabled;
uint16_t unused6;
};
struct EnergyAccountingConfig {
uint16_t thresholds[10];
uint8_t hours;
}; // 21
struct EnergyAccountingConfig101 {
uint8_t thresholds[10];
uint8_t hours;
}; // 11
struct UiConfig {
uint8_t showImport;
uint8_t showExport;
@@ -236,11 +257,14 @@ struct UiConfig {
}; // 15
struct UpgradeInformation {
char fromVersion[8];
char toVersion[8];
int16_t exitCode;
int16_t errorCode;
}; // 20
char fromVersion[16];
char toVersion[16];
uint32_t size;
uint16_t block_position;
uint8_t retry_count;
uint8_t reboot_count;
int8_t errorCode;
}; // 41+3
struct CloudConfig {
bool enabled;
@@ -248,7 +272,14 @@ struct CloudConfig {
char hostname[64];
uint16_t port;
uint8_t clientId[16];
}; // 69
uint8_t proto;
}; // 88
struct ZmartChargeConfig {
bool enabled;
char token[21];
char baseUrl[64];
}; // 86
class AmsConfiguration {
public:
@@ -279,6 +310,8 @@ public:
bool getWebConfig(WebConfig&);
bool setWebConfig(WebConfig&);
void clearWebConfig(WebConfig&);
bool isWebChanged();
void ackWebChange();
bool getMeterConfig(MeterConfig&);
bool setMeterConfig(MeterConfig&);
@@ -333,7 +366,7 @@ public:
void ackUiLanguageChange();
bool getUpgradeInformation(UpgradeInformation&);
bool setUpgradeInformation(int16_t exitCode, int16_t errorCode, const char* currentVersion, const char* nextVersion);
bool setUpgradeInformation(UpgradeInformation&);
void clearUpgradeInformation(UpgradeInformation&);
bool getCloudConfig(CloudConfig&);
@@ -341,6 +374,13 @@ public:
void clearCloudConfig(CloudConfig&);
bool isCloudChanged();
void ackCloudConfig();
bool getZmartChargeConfig(ZmartChargeConfig&);
bool setZmartChargeConfig(ZmartChargeConfig&);
void clearZmartChargeConfig(ZmartChargeConfig&);
bool isZmartChargeConfigChanged();
void ackZmartChargeConfig();
void clear();
@@ -349,10 +389,8 @@ protected:
private:
uint8_t configVersion = 0;
bool sysChanged = false, networkChanged, mqttChanged, meterChanged = true, ntpChanged = true, priceChanged = false, energyAccountingChanged = true, cloudChanged = true, uiLanguageChanged = false;
bool sysChanged = false, networkChanged = false, mqttChanged = false, webChanged = false, meterChanged = true, ntpChanged = true, priceChanged = false, energyAccountingChanged = true, cloudChanged = true, uiLanguageChanged = false, zcChanged = true;
bool relocateConfig101(); // 2.2.0 through 2.2.8
bool relocateConfig102(); // 2.2.9 through 2.2.11
bool relocateConfig103(); // 2.2.12, until, but not including 2.3
void saveToFs();

View File

@@ -7,7 +7,7 @@
#ifndef _AMSSTORAGE_H
#define _AMSSTORAGE_H
#define FILE_FIRMWARE "/firmware.bin"
#define FILE_FIRMWARE_DELETE "/firmware.bin"
#define FILE_MQTT_CA "/mqtt-ca.pem"
#define FILE_MQTT_CERT "/mqtt-cert.pem"

View File

@@ -13,6 +13,7 @@
String toHex(uint8_t* in);
String toHex(uint8_t* in, uint16_t size);
void fromHex(uint8_t *out, String in, uint16_t size);
bool stripNonAscii(uint8_t* in, uint16_t size, bool extended = false);
bool stripNonAscii(uint8_t* in, uint16_t size, bool extended = false, bool trim = true);
void debugPrint(uint8_t *buffer, uint16_t start, uint16_t length, Print* debugger);
#endif

View File

@@ -13,15 +13,26 @@
bool AmsConfiguration::getSystemConfig(SystemConfig& config) {
EEPROM.begin(EEPROM_SIZE);
uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
if(configVersion == EEPROM_CHECK_SUM || configVersion == EEPROM_CLEARED_INDICATOR) {
EEPROM.get(CONFIG_SYSTEM_START, config);
EEPROM.end();
EEPROM.get(CONFIG_SYSTEM_START, config);
EEPROM.end();
if(config.firmwareChannel > 3) {
config.firmwareChannel = 0;
}
if(configVersion == EEPROM_CHECK_SUM) {
return true;
} else {
config.boardType = 0xFF;
config.vendorConfigured = false;
if(configVersion == EEPROM_CLEARED_INDICATOR && config.boardType > 0 && config.boardType < 250) {
config.vendorConfigured = true;
} else {
config.vendorConfigured = false;
config.boardType = 0xFF;
clear();
}
config.userConfigured = false;
config.dataCollectionConsent = 0;
config.firmwareChannel = 0;
config.energyspeedometer = 0;
memset(config.country, 0, 3);
return false;
@@ -37,6 +48,9 @@ bool AmsConfiguration::setSystemConfig(SystemConfig& config) {
sysChanged |= config.dataCollectionConsent != existing.dataCollectionConsent;
sysChanged |= strcmp(config.country, existing.country) != 0;
sysChanged |= config.energyspeedometer != existing.energyspeedometer;
sysChanged |= config.firmwareChannel != existing.firmwareChannel;
} else {
sysChanged = true;
}
EEPROM.begin(EEPROM_SIZE);
stripNonAscii((uint8_t*) config.country, 2);
@@ -91,7 +105,7 @@ bool AmsConfiguration::setNetworkConfig(NetworkConfig& config) {
}
stripNonAscii((uint8_t*) config.ssid, 32, true);
stripNonAscii((uint8_t*) config.psk, 64);
stripNonAscii((uint8_t*) config.psk, 64, true, false);
stripNonAscii((uint8_t*) config.ip, 16);
stripNonAscii((uint8_t*) config.gateway, 16);
stripNonAscii((uint8_t*) config.subnet, 16);
@@ -147,6 +161,18 @@ bool AmsConfiguration::getMqttConfig(MqttConfig& config) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_MQTT_START, config);
EEPROM.end();
if(config.magic != 0xA5) { // New magic for 2.4.11
if(config.magic != 0x9C) {
if(config.magic != 0x7B) {
config.stateUpdate = false;
config.stateUpdateInterval = 10;
}
config.timeout = 1000;
config.keepalive = 60;
}
config.rebootMinutes = config.ssl ? 5 : 0;
config.magic = 0xA5;
}
return true;
} else {
clearMqtt(config);
@@ -166,6 +192,11 @@ bool AmsConfiguration::setMqttConfig(MqttConfig& config) {
mqttChanged |= strcmp(config.password, existing.password) != 0;
mqttChanged |= config.payloadFormat != existing.payloadFormat;
mqttChanged |= config.ssl != existing.ssl;
mqttChanged |= config.stateUpdate != existing.stateUpdate;
mqttChanged |= config.stateUpdateInterval != existing.stateUpdateInterval;
mqttChanged |= config.timeout != existing.timeout;
mqttChanged |= config.keepalive != existing.keepalive;
mqttChanged |= config.rebootMinutes != existing.rebootMinutes;
} else {
mqttChanged = true;
}
@@ -174,8 +205,13 @@ bool AmsConfiguration::setMqttConfig(MqttConfig& config) {
stripNonAscii((uint8_t*) config.clientId, 32);
stripNonAscii((uint8_t*) config.publishTopic, 64);
stripNonAscii((uint8_t*) config.subscribeTopic, 64);
stripNonAscii((uint8_t*) config.username, 128);
stripNonAscii((uint8_t*) config.password, 256);
stripNonAscii((uint8_t*) config.username, 128, true);
stripNonAscii((uint8_t*) config.password, 256, true, false);
if(config.timeout < 500) config.timeout = 1000;
if(config.timeout > 10000) config.timeout = 1000;
if(config.keepalive < 5) config.keepalive = 60;
if(config.keepalive > 240) config.keepalive = 60;
if(config.rebootMinutes > 240) config.rebootMinutes = 0;
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_MQTT_START, config);
@@ -195,6 +231,12 @@ void AmsConfiguration::clearMqtt(MqttConfig& config) {
memset(config.password, 0, 256);
config.payloadFormat = 0;
config.ssl = false;
config.magic = 0x7B;
config.stateUpdate = false;
config.stateUpdateInterval = 10;
config.timeout = 1000;
config.keepalive = 60;
config.rebootMinutes = 0;
}
void AmsConfiguration::setMqttChanged() {
@@ -222,9 +264,17 @@ bool AmsConfiguration::getWebConfig(WebConfig& config) {
}
bool AmsConfiguration::setWebConfig(WebConfig& config) {
WebConfig existing;
if(getWebConfig(existing)) {
webChanged |= strcmp(config.username, existing.username) != 0;
webChanged |= strcmp(config.password, existing.password) != 0;
webChanged |= strcmp(config.context, existing.context) != 0;
} else {
webChanged = true;
}
stripNonAscii((uint8_t*) config.username, 37);
stripNonAscii((uint8_t*) config.password, 37);
stripNonAscii((uint8_t*) config.password, 37, false, false);
stripNonAscii((uint8_t*) config.context, 37);
EEPROM.begin(EEPROM_SIZE);
@@ -241,9 +291,18 @@ void AmsConfiguration::clearWebConfig(WebConfig& config) {
memset(config.context, 0, 37);
}
bool AmsConfiguration::isWebChanged() {
return webChanged;
}
void AmsConfiguration::ackWebChange() {
webChanged = false;
}
bool AmsConfiguration::getMeterConfig(MeterConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(EEPROM_SIZE);
uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
if(configVersion == EEPROM_CHECK_SUM || configVersion == EEPROM_CLEARED_INDICATOR) {
EEPROM.get(CONFIG_METER_START, config);
EEPROM.end();
if(config.bufferSize < 1 || config.bufferSize > 64) {
@@ -400,6 +459,7 @@ bool AmsConfiguration::getHomeAssistantConfig(HomeAssistantConfig& config) {
EEPROM.end();
if(stripNonAscii((uint8_t*) config.discoveryPrefix, 64) || stripNonAscii((uint8_t*) config.discoveryHostname, 64) || stripNonAscii((uint8_t*) config.discoveryNameTag, 16)) {
clearHomeAssistantConfig(config);
return false;
}
return true;
} else {
@@ -457,6 +517,7 @@ bool AmsConfiguration::getGpioConfig(GpioConfig& config) {
if(configVersion == EEPROM_CHECK_SUM || configVersion == EEPROM_CLEARED_INDICATOR) {
EEPROM.get(CONFIG_GPIO_START, config);
EEPROM.end();
if(config.powersaving > 4) config.powersaving = 0;
return true;
} else {
clearGpio(config);
@@ -530,6 +591,7 @@ void AmsConfiguration::clearGpio(GpioConfig& config, bool all) {
config.tempAnalogSensorPin = 0xFF;
config.vccPin = 0xFF;
config.ledDisablePin = 0xFF;
config.powersaving = 0;
if(all) {
config.vccOffset = 0;
@@ -602,6 +664,10 @@ bool AmsConfiguration::getPriceServiceConfig(PriceServiceConfig& config) {
EEPROM.end();
if(strlen(config.entsoeToken) != 0 && strlen(config.entsoeToken) != 36) {
clearPriceServiceConfig(config);
return false;
}
if(config.resolutionInMinutes != 15 && config.resolutionInMinutes != 60) {
config.resolutionInMinutes = 60;
}
return true;
} else {
@@ -617,6 +683,7 @@ bool AmsConfiguration::setPriceServiceConfig(PriceServiceConfig& config) {
priceChanged |= strcmp(config.area, existing.area) != 0;
priceChanged |= strcmp(config.currency, existing.currency) != 0;
priceChanged |= config.enabled != existing.enabled;
priceChanged |= config.resolutionInMinutes != existing.resolutionInMinutes;
} else {
priceChanged = true;
}
@@ -636,9 +703,8 @@ void AmsConfiguration::clearPriceServiceConfig(PriceServiceConfig& config) {
memset(config.entsoeToken, 0, 37);
memset(config.area, 0, 17);
memset(config.currency, 0, 4);
config.unused1 = 1000;
config.enabled = false;
config.unused2 = 0;
config.resolutionInMinutes = 60;
}
bool AmsConfiguration::isPriceServiceChanged() {
@@ -657,6 +723,7 @@ bool AmsConfiguration::getEnergyAccountingConfig(EnergyAccountingConfig& config)
EEPROM.end();
if(config.thresholds[9] != 0xFFFF) {
clearEnergyAccountingConfig(config);
return false;
}
if(config.hours > 5) config.hours = 5;
return true;
@@ -764,15 +831,9 @@ void AmsConfiguration::ackUiLanguageChange() {
uiLanguageChanged = false;
}
bool AmsConfiguration::setUpgradeInformation(int16_t exitCode, int16_t errorCode, const char* currentVersion, const char* nextVersion) {
UpgradeInformation upinfo;
upinfo.exitCode = exitCode;
upinfo.errorCode = errorCode;
strcpy(upinfo.fromVersion, currentVersion);
strcpy(upinfo.toVersion, nextVersion);
stripNonAscii((uint8_t*) upinfo.fromVersion, 8);
stripNonAscii((uint8_t*) upinfo.toVersion, 8);
bool AmsConfiguration::setUpgradeInformation(UpgradeInformation& upinfo) {
stripNonAscii((uint8_t*) upinfo.fromVersion, 16);
stripNonAscii((uint8_t*) upinfo.toVersion, 16);
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_UPGRADE_INFO_START, upinfo);
@@ -786,8 +847,9 @@ bool AmsConfiguration::getUpgradeInformation(UpgradeInformation& upinfo) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_UPGRADE_INFO_START, upinfo);
EEPROM.end();
if(stripNonAscii((uint8_t*) upinfo.fromVersion, 8) || stripNonAscii((uint8_t*) upinfo.toVersion, 8)) {
if(stripNonAscii((uint8_t*) upinfo.fromVersion, 16) || stripNonAscii((uint8_t*) upinfo.toVersion, 16)) {
clearUpgradeInformation(upinfo);
return false;
}
return true;
} else {
@@ -797,10 +859,13 @@ bool AmsConfiguration::getUpgradeInformation(UpgradeInformation& upinfo) {
}
void AmsConfiguration::clearUpgradeInformation(UpgradeInformation& upinfo) {
upinfo.exitCode = -1;
memset(upinfo.fromVersion, 0, 16);
memset(upinfo.toVersion, 0, 16);
upinfo.errorCode = 0;
memset(upinfo.fromVersion, 0, 8);
memset(upinfo.toVersion, 0, 8);
upinfo.size = 0;
upinfo.block_position = 0;
upinfo.retry_count = 0;
upinfo.reboot_count = 0;
}
bool AmsConfiguration::getCloudConfig(CloudConfig& config) {
@@ -808,6 +873,7 @@ bool AmsConfiguration::getCloudConfig(CloudConfig& config) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_CLOUD_START, config);
EEPROM.end();
if(config.proto > 2) config.proto = 0;
return true;
} else {
clearCloudConfig(config);
@@ -821,6 +887,7 @@ bool AmsConfiguration::setCloudConfig(CloudConfig& config) {
cloudChanged |= config.enabled != existing.enabled;
cloudChanged |= config.interval!= existing.interval;
cloudChanged |= config.port!= existing.port;
cloudChanged |= config.proto!= existing.proto;
cloudChanged |= strcmp(config.hostname, existing.hostname) != 0;
cloudChanged |= memcmp(config.clientId, existing.clientId, 16) != 0;
} else {
@@ -839,6 +906,7 @@ bool AmsConfiguration::setCloudConfig(CloudConfig& config) {
void AmsConfiguration::clearCloudConfig(CloudConfig& config) {
config.enabled = false;
strcpy_P(config.hostname, PSTR("cloud.amsleser.no"));
config.proto = 1;
config.port = 7443;
config.interval = 10;
memset(config.clientId, 0, 16);
@@ -852,6 +920,65 @@ void AmsConfiguration::ackCloudConfig() {
cloudChanged = false;
}
bool AmsConfiguration::getZmartChargeConfig(ZmartChargeConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_ZC_START, config);
EEPROM.end();
stripNonAscii((uint8_t*) config.token, 21);
stripNonAscii((uint8_t*) config.baseUrl, 64);
if(strlen(config.token) != 20 || !config.enabled) {
config.enabled = false;
memset(config.token, 0, 64);
memset(config.baseUrl, 0, 64);
}
if(strlen(config.baseUrl) == 0 || strncmp_P(config.baseUrl, PSTR("https"), 5) != 0) {
memset(config.baseUrl, 0, 64);
snprintf_P(config.baseUrl, 64, PSTR("https://main.zmartcharge.com/api"));
}
return true;
} else {
clearZmartChargeConfig(config);
return false;
}
}
bool AmsConfiguration::setZmartChargeConfig(ZmartChargeConfig& config) {
ZmartChargeConfig existing;
if(getZmartChargeConfig(existing)) {
zcChanged |= config.enabled != existing.enabled;
zcChanged |= strcmp(config.token, existing.token) != 0;
zcChanged |= strcmp(config.baseUrl, existing.baseUrl) != 0;
} else {
zcChanged = true;
}
stripNonAscii((uint8_t*) config.token, 21);
stripNonAscii((uint8_t*) config.baseUrl, 64);
if(strncmp_P(config.baseUrl, PSTR("https"), 5) != 0) {
memset(config.baseUrl, 0, 64);
}
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_ZC_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
void AmsConfiguration::clearZmartChargeConfig(ZmartChargeConfig& config) {
config.enabled = false;
memset(config.token, 0, 21);
}
bool AmsConfiguration::isZmartChargeConfigChanged() {
return zcChanged;
}
void AmsConfiguration::ackZmartChargeConfig() {
zcChanged = false;
}
void AmsConfiguration::setUiLanguageChanged() {
uiLanguageChanged = true;
}
@@ -863,6 +990,7 @@ void AmsConfiguration::clear() {
EEPROM.get(CONFIG_SYSTEM_START, sys);
sys.userConfigured = false;
sys.dataCollectionConsent = 0;
sys.firmwareChannel = 0;
sys.energyspeedometer = 0;
memset(sys.country, 0, 3);
EEPROM.put(CONFIG_SYSTEM_START, sys);
@@ -919,6 +1047,10 @@ void AmsConfiguration::clear() {
clearCloudConfig(cloud);
EEPROM.put(CONFIG_CLOUD_START, cloud);
ZmartChargeConfig zc;
clearZmartChargeConfig(zc);
EEPROM.put(CONFIG_ZC_START, zc);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CLEARED_INDICATOR);
EEPROM.commit();
EEPROM.end();
@@ -938,22 +1070,6 @@ bool AmsConfiguration::hasConfig() {
}
} else {
switch(configVersion) {
case 101:
configVersion = -1; // Prevent loop
if(relocateConfig101()) {
configVersion = 102;
} else {
configVersion = 0;
return false;
}
case 102:
configVersion = -1; // Prevent loop
if(relocateConfig102()) {
configVersion = 103;
} else {
configVersion = 0;
return false;
}
case 103:
configVersion = -1; // Prevent loop
if(relocateConfig103()) {
@@ -976,48 +1092,6 @@ int AmsConfiguration::getConfigVersion() {
return configVersion;
}
bool AmsConfiguration::relocateConfig101() {
EEPROM.begin(EEPROM_SIZE);
EnergyAccountingConfig config;
EnergyAccountingConfig101 config101;
EEPROM.get(CONFIG_ENERGYACCOUNTING_START_103, config101);
for(uint8_t i = 0; i < 9; i++) {
config.thresholds[i] = config101.thresholds[i];
}
config.thresholds[9] = 0xFFFF;
config.hours = config101.hours;
EEPROM.put(CONFIG_ENERGYACCOUNTING_START_103, config);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 102);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig102() {
EEPROM.begin(EEPROM_SIZE);
GpioConfig103 gpioConfig;
EEPROM.get(CONFIG_GPIO_START_103, gpioConfig);
gpioConfig.hanPinPullup = true;
EEPROM.put(CONFIG_GPIO_START_103, gpioConfig);
HomeAssistantConfig haconf;
clearHomeAssistantConfig(haconf);
EEPROM.put(CONFIG_HA_START_103, haconf);
PriceServiceConfig entsoe;
EEPROM.get(CONFIG_ENTSOE_START_103, entsoe);
entsoe.unused2 = 0;
EEPROM.put(CONFIG_ENTSOE_START_103, entsoe);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 103);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig103() {
EEPROM.begin(EEPROM_SIZE);
@@ -1079,12 +1153,13 @@ bool AmsConfiguration::relocateConfig103() {
gpio103.vccResistorGnd,
gpio103.vccResistorVcc,
gpio103.ledDisablePin,
gpio103.ledBehaviour
gpio103.ledBehaviour,
0
};
WebConfig web = {web103.security};
strcpy_P(web.username, web103.username);
strcpy_P(web.password, web103.password);
strcpy(web.username, web103.username);
strcpy(web.password, web103.password);
memset(web.context, 0, 37);
strcpy_P(ui.language, PSTR("en"));
@@ -1111,6 +1186,10 @@ bool AmsConfiguration::relocateConfig103() {
clearCloudConfig(cloud);
EEPROM.put(CONFIG_CLOUD_START, cloud);
ZmartChargeConfig zcc;
clearZmartChargeConfig(zcc);
EEPROM.put(CONFIG_ZC_START, zcc);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 104);
bool ret = EEPROM.commit();
EEPROM.end();
@@ -1119,6 +1198,7 @@ bool AmsConfiguration::relocateConfig103() {
bool AmsConfiguration::save() {
EEPROM.begin(EEPROM_SIZE);
uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM);
bool success = EEPROM.commit();
EEPROM.end();
@@ -1190,6 +1270,9 @@ void AmsConfiguration::print(Print* debugger)
}
debugger->printf_P(PSTR("Payload format: %i\r\n"), mqtt.payloadFormat);
debugger->printf_P(PSTR("SSL: %s\r\n"), mqtt.ssl ? "Yes" : "No");
debugger->printf_P(PSTR("Timeout: %i\r\n"), mqtt.timeout);
debugger->printf_P(PSTR("Keep-alive: %i\r\n"), mqtt.keepalive);
debugger->printf_P(PSTR("Auto reboot minutes: %i\r\n"), mqtt.rebootMinutes);
} else {
debugger->printf_P(PSTR("Enabled: No\r\n"));
}
@@ -1243,6 +1326,7 @@ void AmsConfiguration::print(Print* debugger)
debugger->printf_P(PSTR("Vcc pin: %i\r\n"), gpio.vccPin);
debugger->printf_P(PSTR("LED disable pin: %i\r\n"), gpio.ledDisablePin);
debugger->printf_P(PSTR("LED behaviour: %i\r\n"), gpio.ledBehaviour);
debugger->printf_P(PSTR("Power saving: %i\r\n"), gpio.powersaving);
if(gpio.vccMultiplier > 0) {
debugger->printf_P(PSTR("Vcc multiplier: %f\r\n"), gpio.vccMultiplier / 1000.0);
}
@@ -1298,27 +1382,50 @@ void AmsConfiguration::print(Print* debugger)
debugger->printf_P(PSTR("Area: %s\r\n"), price.area);
debugger->printf_P(PSTR("Currency: %s\r\n"), price.currency);
debugger->printf_P(PSTR("ENTSO-E Token: %s\r\n"), price.entsoeToken);
debugger->println(F(""));
delay(10);
debugger->flush();
}
debugger->println(F(""));
delay(10);
debugger->flush();
}
UiConfig ui;
if(getUiConfig(ui)) {
debugger->println(F("--UI configuration--"));
debugger->printf_P(PSTR("Language: %s\r\n"), ui.language);
debugger->println(F(""));
delay(10);
debugger->flush();
}
#if defined(ESP32)
CloudConfig cc;
if(getCloudConfig(cc)) {
String uuid = ESPRandom::uuidToString(cc.clientId);;
debugger->println(F("--UI configuration--"));
debugger->println(F("--Cloud configuration--"));
debugger->printf_P(PSTR("Enabled: %s\r\n"), cc.enabled ? "Yes" : "No");
debugger->printf_P(PSTR("Hostname: %s\r\n"), cc.hostname);
debugger->printf_P(PSTR("Client ID: %s\r\n"), uuid.c_str());
debugger->printf_P(PSTR("Interval: %d\r\n"), cc.interval);
if(cc.enabled) {
debugger->printf_P(PSTR("Hostname: %s\r\n"), cc.hostname);
debugger->printf_P(PSTR("Client ID: %s\r\n"), uuid.c_str());
debugger->printf_P(PSTR("Interval: %d\r\n"), cc.interval);
}
debugger->println(F(""));
delay(10);
debugger->flush();
}
#endif
#if defined(ZMART_CHARGE)
ZmartChargeConfig zc;
if(getZmartChargeConfig(zc)) {
debugger->println(F("--ZmartCharge configuration--"));
debugger->printf_P(PSTR("Enabled: %s\r\n"), zc.enabled ? "Yes" : "No");
if(zc.enabled) {
debugger->printf_P(PSTR("Base URL: '%s'\r\n"), zc.baseUrl);
debugger->printf_P(PSTR("Token: '%s'\r\n"), zc.token);
}
debugger->println(F(""));
delay(10);
debugger->flush();
}
#endif

View File

@@ -28,14 +28,14 @@ void fromHex(uint8_t *out, String in, uint16_t size) {
}
}
bool stripNonAscii(uint8_t* in, uint16_t size, bool extended) {
bool stripNonAscii(uint8_t* in, uint16_t size, bool extended, bool trim) {
bool ret = false;
for(uint16_t i = 0; i < size; i++) {
if(in[i] == 0) { // Clear the rest with null-terminator
memset(in+i, 0, size-i);
break;
}
if(extended && (in[i] < 32 || in[i] == 127 || in[i] == 129 || in[i] == 141 || in[i] == 143 || in[i] == 144 || in[i] == 157)) {
if(extended && (in[i] < 32 || in[i] == 127 || in[i] == 129 || in[i] == 141 || in[i] == 143 || in[i] == 144 || in[i] == 157 || in[i] == 160)) {
memset(in+i, ' ', 1);
ret = true;
} else if(!extended && (in[i] < 32 || in[i] > 126)) {
@@ -43,6 +43,38 @@ bool stripNonAscii(uint8_t* in, uint16_t size, bool extended) {
ret = true;
}
}
if(trim) {
// Strip leading spaces
while(in[0] == ' ') {
for(uint16_t i = 0; i < size; i++) {
in[i] = in[i+1];
}
}
// Strip trailing spaces
for(int i = size-1; i > 0; i--) {
if(in[i] == ' ' || in[i] == 0) {
memset(in+i, 0, 1);
} else {
break;
}
}
}
memset(in+size-1, 0, 1); // Make sure the last character is null-terminator
return ret;
}
void debugPrint(uint8_t *buffer, uint16_t start, uint16_t length, Print* debugger) {
for (uint16_t i = start; i < start + length; i++) {
if (buffer[i] < 0x10)
debugger->print(F("0"));
debugger->print(buffer[i], HEX);
debugger->print(F(" "));
if ((i - start + 1) % 16 == 0)
debugger->println(F(""));
else if ((i - start + 1) % 4 == 0)
debugger->print(F(" "));
yield(); // Let other get some resources too
}
debugger->println(F(""));
}

View File

@@ -105,6 +105,7 @@ protected:
double l1activeExportCounter = 0, l2activeExportCounter = 0, l3activeExportCounter = 0;
float powerFactor = 0, l1PowerFactor = 0, l2PowerFactor = 0, l3PowerFactor = 0;
double activeImportCounter = 0, reactiveImportCounter = 0, activeExportCounter = 0, reactiveExportCounter = 0;
double lastKnownCounter = 0;
bool threePhase = false, twoPhase = false, counterEstimated = false, l2currentMissing = false;;
int8_t lastError = 0x00;

View File

@@ -59,6 +59,10 @@ const OBIS_code_t OBIS_POWER_FACTOR PROGMEM = { 13, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_IMPORT_L1 PROGMEM = { 21, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_EXPORT_L1 PROGMEM = { 22, 7, 0 };
const OBIS_code_t OBIS_REACTIVE_IMPORT_L1 PROGMEM = { 23, 7, 0 };
const OBIS_code_t OBIS_REACTIVE_EXPORT_L1 PROGMEM = { 24, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_IMPORT_COUNT_L1 PROGMEM ={ 21, 8, 0 };
const OBIS_code_t OBIS_REACTIVE_IMPORT_COUNT_L1 PROGMEM ={ 22, 8, 0 };
const OBIS_code_t OBIS_CURRENT_L1 PROGMEM = { 31, 7, 0 };
const OBIS_code_t OBIS_VOLTAGE_L1 PROGMEM = { 32, 7, 0 };
@@ -66,6 +70,10 @@ const OBIS_code_t OBIS_POWER_FACTOR_L1 PROGMEM = { 33, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_IMPORT_L2 PROGMEM = { 41, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_EXPORT_L2 PROGMEM = { 42, 7, 0 };
const OBIS_code_t OBIS_REACTIVE_IMPORT_L2 PROGMEM = { 43, 7, 0 };
const OBIS_code_t OBIS_REACTIVE_EXPORT_L2 PROGMEM = { 44, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_IMPORT_COUNT_L2 PROGMEM ={ 41, 8, 0 };
const OBIS_code_t OBIS_REACTIVE_IMPORT_COUNT_L2 PROGMEM ={ 42, 8, 0 };
const OBIS_code_t OBIS_CURRENT_L2 PROGMEM = { 51, 7, 0 };
const OBIS_code_t OBIS_VOLTAGE_L2 PROGMEM = { 52, 7, 0 };
@@ -73,6 +81,10 @@ const OBIS_code_t OBIS_POWER_FACTOR_L2 PROGMEM = { 53, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_IMPORT_L3 PROGMEM = { 61, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_EXPORT_L3 PROGMEM = { 62, 7, 0 };
const OBIS_code_t OBIS_REACTIVE_IMPORT_L3 PROGMEM = { 63, 7, 0 };
const OBIS_code_t OBIS_REACTIVE_EXPORT_L3 PROGMEM = { 64, 7, 0 };
const OBIS_code_t OBIS_ACTIVE_IMPORT_COUNT_L3 PROGMEM ={ 61, 8, 0 };
const OBIS_code_t OBIS_REACTIVE_IMPORT_COUNT_L3 PROGMEM ={ 62, 8, 0 };
const OBIS_code_t OBIS_CURRENT_L3 PROGMEM = { 71, 7, 0 };
const OBIS_code_t OBIS_VOLTAGE_L3 PROGMEM = { 72, 7, 0 };

View File

@@ -69,10 +69,23 @@ void AmsData::apply(AmsData& other) {
this->l3activeExportCounter = other.getL3ActiveExportCounter();
case 3:
this->meterTimestamp = other.getMeterTimestamp();
this->activeImportCounter = other.getActiveImportCounter();
this->activeExportCounter = other.getActiveExportCounter();
this->reactiveImportCounter = other.getReactiveImportCounter();
this->reactiveExportCounter = other.getReactiveExportCounter();
// Aidon tends to sometime send the same counter as last hour by accident
if(meterType == AmsTypeAidon && counterEstimated && lastKnownCounter == other.getActiveImportCounter()-other.getActiveExportCounter()) {
double diff = activeImportCounter - activeExportCounter - lastKnownCounter;
if(diff < 1.0) { // In case a very low value have been calculated, use the new values
this->activeImportCounter = other.getActiveImportCounter();
this->activeExportCounter = other.getActiveExportCounter();
this->reactiveImportCounter = other.getReactiveImportCounter();
this->reactiveExportCounter = other.getReactiveExportCounter();
this->lastKnownCounter = activeImportCounter - activeExportCounter;
}
} else {
this->activeImportCounter = other.getActiveImportCounter();
this->activeExportCounter = other.getActiveExportCounter();
this->reactiveImportCounter = other.getReactiveImportCounter();
this->reactiveExportCounter = other.getReactiveExportCounter();
this->lastKnownCounter = activeImportCounter - activeExportCounter;
}
this->counterEstimated = false;
case 2:
this->listId = other.getListId();
@@ -100,88 +113,156 @@ void AmsData::apply(AmsData& other) {
}
void AmsData::apply(OBIS_code_t obis, double value) {
switch(obis.gr) {
case 1:
switch(obis.sensor) {
case 7:
switch(obis.tariff) {
case 0:
activeImportPower = value;
listType = max(listType, (uint8_t) 1);
break;
}
break;
case 8:
switch(obis.tariff) {
case 0:
activeImportCounter = value;
listType = max(listType, (uint8_t) 3);
break;
}
break;
}
break;
case 2:
switch(obis.sensor) {
case 7:
switch(obis.tariff) {
case 0:
activeExportPower = value;
listType = max(listType, (uint8_t) 2);
break;
}
break;
case 8:
switch(obis.tariff) {
case 0:
activeExportCounter = value;
listType = max(listType, (uint8_t) 3);
break;
}
break;
}
break;
case 3:
switch(obis.sensor) {
case 7:
switch(obis.tariff) {
case 0:
reactiveImportPower = value;
listType = max(listType, (uint8_t) 2);
break;
}
break;
case 8:
switch(obis.tariff) {
case 0:
reactiveImportCounter = value;
listType = max(listType, (uint8_t) 3);
break;
}
break;
}
break;
case 4:
switch(obis.sensor) {
case 7:
switch(obis.tariff) {
case 0:
reactiveExportPower = value;
listType = max(listType, (uint8_t) 2);
break;
}
break;
case 8:
switch(obis.tariff) {
case 0:
reactiveExportCounter = value;
listType = max(listType, (uint8_t) 3);
break;
}
break;
}
break;
if(obis.sensor == 0 && obis.gr == 0 && obis.tariff == 0) {
meterType = value;
}
if(obis.gr == 1) {
if(obis.sensor == 96) {
if(obis.tariff == 0) {
meterId = String((long) value, 10);
return;
} else if(obis.tariff == 1) {
return;
}
}
}
if(obis.tariff != 0) {
Serial.println("Tariff not implemented");
return;
}
if(obis.gr == 7) { // Instant values
switch(obis.sensor) {
case 1:
activeImportPower = value;
listType = max(listType, (uint8_t) 2);
break;
case 2:
activeExportPower = value;
listType = max(listType, (uint8_t) 2);
break;
case 3:
reactiveImportPower = value;
listType = max(listType, (uint8_t) 2);
break;
case 4:
reactiveExportPower = value;
listType = max(listType, (uint8_t) 2);
break;
case 13:
powerFactor = value;
listType = max(listType, (uint8_t) 4);
break;
case 21:
l1activeImportPower = value;
listType = max(listType, (uint8_t) 4);
break;
case 22:
l1activeExportPower = value;
listType = max(listType, (uint8_t) 4);
break;
case 31:
l1current = value;
listType = max(listType, (uint8_t) 2);
break;
case 32:
l1voltage = value;
listType = max(listType, (uint8_t) 2);
break;
case 33:
l1PowerFactor = value;
listType = max(listType, (uint8_t) 4);
break;
case 41:
l2activeImportPower = value;
listType = max(listType, (uint8_t) 4);
break;
case 42:
l2activeExportPower = value;
listType = max(listType, (uint8_t) 4);
break;
case 51:
l2current = value;
listType = max(listType, (uint8_t) 2);
break;
case 52:
l2voltage = value;
listType = max(listType, (uint8_t) 2);
break;
case 53:
l2PowerFactor = value;
listType = max(listType, (uint8_t) 4);
break;
case 61:
l3activeImportPower = value;
listType = max(listType, (uint8_t) 4);
break;
case 62:
l3activeExportPower = value;
listType = max(listType, (uint8_t) 4);
break;
case 71:
l3current = value;
listType = max(listType, (uint8_t) 2);
break;
case 72:
l3voltage = value;
listType = max(listType, (uint8_t) 2);
break;
case 73:
l3PowerFactor = value;
listType = max(listType, (uint8_t) 4);
break;
}
} else if(obis.gr == 8) { // Accumulated values
switch(obis.sensor) {
case 1:
activeImportCounter = value;
listType = max(listType, (uint8_t) 3);
break;
case 2:
activeExportCounter = value;
listType = max(listType, (uint8_t) 3);
break;
case 3:
reactiveImportCounter = value;
listType = max(listType, (uint8_t) 3);
break;
case 4:
reactiveExportCounter = value;
listType = max(listType, (uint8_t) 3);
break;
case 21:
l1activeImportCounter = value;
listType = max(listType, (uint8_t) 4);
break;
case 22:
l1activeExportCounter = value;
listType = max(listType, (uint8_t) 4);
break;
case 41:
l2activeImportCounter = value;
listType = max(listType, (uint8_t) 4);
break;
case 42:
l2activeExportCounter = value;
listType = max(listType, (uint8_t) 4);
break;
case 61:
l3activeImportCounter = value;
listType = max(listType, (uint8_t) 4);
break;
case 62:
l3activeExportCounter = value;
listType = max(listType, (uint8_t) 4);
break;
}
}
if(listType > 0)
lastUpdateMillis = millis();
threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0;
if(!threePhase)
twoPhase = (l1voltage > 0 && l2voltage > 0) || (l2voltage > 0 && l3voltage > 0) || (l3voltage > 0 && l1voltage > 0);
}
uint64_t AmsData::getLastUpdateMillis() {

View File

@@ -8,7 +8,9 @@
#define _AMSDATASTORAGE_H
#include "Arduino.h"
#include "AmsData.h"
#if defined(AMS_REMOTE_DEBUG)
#include "RemoteDebug.h"
#endif
#include "Timezone.h"
struct DayDataPoints5 {
@@ -53,9 +55,13 @@ struct MonthDataPoints {
class AmsDataStorage {
public:
#if defined(AMS_REMOTE_DEBUG)
AmsDataStorage(RemoteDebug*);
#else
AmsDataStorage(Stream*);
#endif
void setTimezone(Timezone*);
bool update(AmsData*);
bool update(AmsData* data, time_t now);
uint32_t getHourImport(uint8_t);
uint32_t getHourExport(uint8_t);
uint32_t getDayImport(uint8_t);
@@ -73,12 +79,17 @@ public:
uint8_t getMonthAccuracy();
void setMonthAccuracy(uint8_t);
bool isHappy();
bool isDayHappy();
bool isMonthHappy();
bool isHappy(time_t now);
bool isDayHappy(time_t now);
bool isMonthHappy(time_t now);
double getEstimatedImportCounter();
void setHourImport(uint8_t, uint32_t);
void setHourExport(uint8_t, uint32_t);
void setDayImport(uint8_t, uint32_t);
void setDayExport(uint8_t, uint32_t);
private:
Timezone* tz;
DayDataPoints day = {
@@ -95,11 +106,11 @@ private:
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
10
};
#if defined(AMS_REMOTE_DEBUG)
RemoteDebug* debugger;
void setHourImport(uint8_t, uint32_t);
void setHourExport(uint8_t, uint32_t);
void setDayImport(uint8_t, uint32_t);
void setDayExport(uint8_t, uint32_t);
#else
Stream* debugger;
#endif
};
#endif

View File

@@ -10,7 +10,11 @@
#include "AmsStorage.h"
#include "FirmwareVersion.h"
#if defined(AMS_REMOTE_DEBUG)
AmsDataStorage::AmsDataStorage(RemoteDebug* debugger) {
#else
AmsDataStorage::AmsDataStorage(Stream* debugger) {
#endif
day.version = 6;
day.accuracy = 1;
month.version = 7;
@@ -22,13 +26,20 @@ void AmsDataStorage::setTimezone(Timezone* tz) {
this->tz = tz;
}
bool AmsDataStorage::update(AmsData* data) {
if(isHappy()) {
bool AmsDataStorage::update(AmsData* data, time_t now) {
if(isHappy(now)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Happy, not updating\n"));
return false;
}
time_t now = time(nullptr);
if(tz == NULL) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("No timezone, not updating\n"));
return false;
}
if(now < FirmwareVersion::BuildEpoch) {
@@ -39,6 +50,10 @@ bool AmsDataStorage::update(AmsData* data) {
}
}
if(now < FirmwareVersion::BuildEpoch) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Before build time, not updating\n"));
return false;
}
@@ -52,11 +67,31 @@ bool AmsDataStorage::update(AmsData* data) {
uint64_t exportCounter = data->getActiveExportCounter() * 1000;
// Clear hours between last update and now
if(day.lastMeterReadTime > now) {
if(!isDayHappy(now) && day.lastMeterReadTime > now) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Day was updated in the future, resetting\n"));
day.activeImport = importCounter;
day.activeExport = exportCounter;
day.lastMeterReadTime = now;
} else if((importCounter > 0 && day.activeImport == 0) || now - day.lastMeterReadTime > 86400) {
} else if(importCounter > 0 && day.activeImport == 0) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Initializing day data\n"));
day.activeImport = importCounter;
day.activeExport = exportCounter;
day.lastMeterReadTime = now;
for(int i = 0; i<24; i++) {
setHourImport(i, 0);
setHourExport(i, 0);
}
} else if(now - day.lastMeterReadTime > 86400) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Day was updated to long ago, clearing\n"));
day.activeImport = importCounter;
day.activeExport = exportCounter;
day.lastMeterReadTime = now;
@@ -68,6 +103,10 @@ bool AmsDataStorage::update(AmsData* data) {
tmElements_t last;
breakTime(day.lastMeterReadTime, last);
uint8_t endHour = utc.Hour;
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Clearing hours from %d to %d\n"), last.Hour, endHour);
if(last.Hour > utc.Hour){
for(int i = 0; i < utc.Hour; i++) {
setHourImport(i, 0);
@@ -82,11 +121,31 @@ bool AmsDataStorage::update(AmsData* data) {
}
// Clear days between last update and now
if(month.lastMeterReadTime > now) {
if(!isMonthHappy(now) && month.lastMeterReadTime > now) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Month was updated in the future, resetting\n"));
month.activeImport = importCounter;
month.activeExport = exportCounter;
month.lastMeterReadTime = now;
} else if((importCounter > 0 && month.activeImport == 0) || now - month.lastMeterReadTime > 2682000) {
} else if(importCounter > 0 && month.activeImport == 0) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Initializing month data\n"));
month.activeImport = importCounter;
month.activeExport = exportCounter;
month.lastMeterReadTime = now;
for(int i = 1; i<=31; i++) {
setDayImport(i, 0);
setDayExport(i, 0);
}
} else if(now - month.lastMeterReadTime > 2682000) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Month was updated to long ago, clearing\n"));
month.activeImport = importCounter;
month.activeExport = exportCounter;
month.lastMeterReadTime = now;
@@ -98,6 +157,10 @@ bool AmsDataStorage::update(AmsData* data) {
tmElements_t last;
breakTime(tz->toLocal(month.lastMeterReadTime), last);
uint8_t endDay = ltz.Day;
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Clearing days from %d to %d\n"), last.Day, endDay);
if(last.Day > ltz.Day) {
for(int i = 1; i < ltz.Day; i++) {
setDayImport(i, 0);
@@ -112,20 +175,36 @@ bool AmsDataStorage::update(AmsData* data) {
}
if(data->getListType() < 3) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Type %d, not updating\n"), data->getListType());
return false;
}
bool ret = false;
// Update day plot
if(!isDayHappy()) {
if(!isDayHappy(now)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Day is not happy\n"));
if(day.activeImport > importCounter || day.activeExport > exportCounter) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR(" - reset\n"));
day.activeImport = importCounter;
day.activeExport = exportCounter;
day.lastMeterReadTime = now;
setHourImport(utcYesterday.Hour, 0);
setHourExport(utcYesterday.Hour, 0);
} else if(now - day.lastMeterReadTime < 4000) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR(" - normal\n"));
uint32_t imp = importCounter - day.activeImport;
uint32_t exp = exportCounter - day.activeExport;
setHourImport(utcYesterday.Hour, imp);
@@ -135,6 +214,10 @@ bool AmsDataStorage::update(AmsData* data) {
day.activeExport = exportCounter;
day.lastMeterReadTime = now;
} else {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR(" - average\n"));
float mins = (now - day.lastMeterReadTime) / 60.0;
uint32_t im = importCounter - day.activeImport;
uint32_t ex = exportCounter - day.activeExport;
@@ -165,14 +248,26 @@ bool AmsDataStorage::update(AmsData* data) {
}
// Update month plot
if(ltz.Hour == 0 && !isMonthHappy()) {
if(ltz.Hour == 0 && !isMonthHappy(now)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Month is not happy\n"));
if(month.activeImport > importCounter || month.activeExport > exportCounter) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR(" - reset\n"));
month.activeImport = importCounter;
month.activeExport = exportCounter;
month.lastMeterReadTime = now;
setDayImport(ltzYesterDay.Day, 0);
setDayExport(ltzYesterDay.Day, 0);
} else if(now - month.lastMeterReadTime < 90100 && now - month.lastMeterReadTime > 82700) { // DST days are 23h (82800s) and 25h (90000)
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR(" - normal\n"));
uint32_t imp = importCounter - month.activeImport;
uint32_t exp = exportCounter - month.activeExport;
@@ -182,6 +277,10 @@ bool AmsDataStorage::update(AmsData* data) {
month.activeExport = exportCounter;
month.lastMeterReadTime = now;
} else {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR(" - average\n"));
// Make sure last month read is at midnight
tmElements_t last;
breakTime(tz->toLocal(month.lastMeterReadTime), last);
@@ -531,22 +630,23 @@ void AmsDataStorage::setMonthAccuracy(uint8_t accuracy) {
month.accuracy = accuracy;
}
bool AmsDataStorage::isHappy() {
return isDayHappy() && isMonthHappy();
bool AmsDataStorage::isHappy(time_t now) {
return isDayHappy(now) && isMonthHappy(now);
}
bool AmsDataStorage::isDayHappy() {
bool AmsDataStorage::isDayHappy(time_t now) {
if(tz == NULL) {
return false;
}
time_t now = time(nullptr);
if(now < FirmwareVersion::BuildEpoch) return false;
if(now < day.lastMeterReadTime) {
return false;
}
if(now-day.lastMeterReadTime > 3600) {
// There are cases where the meter reports before the hour. The update method will then receive the meter timestamp as reference, thus there will not be 3600s between.
// Leaving a 100s buffer for these cases
if(now-day.lastMeterReadTime > 3500) {
return false;
}
@@ -560,26 +660,26 @@ bool AmsDataStorage::isDayHappy() {
return true;
}
bool AmsDataStorage::isMonthHappy() {
bool AmsDataStorage::isMonthHappy(time_t now) {
if(tz == NULL) {
return false;
}
time_t now = time(nullptr);
if(now < FirmwareVersion::BuildEpoch) return false;
tmElements_t tm, last;
if(now < month.lastMeterReadTime) {
return false;
}
breakTime(tz->toLocal(now), tm);
breakTime(tz->toLocal(month.lastMeterReadTime), last);
if(tm.Day != last.Day) {
// 25 hours, because of DST
if(now-month.lastMeterReadTime > 90000) {
return false;
}
if(now-month.lastMeterReadTime > 90100) {
tmElements_t tm, last;
breakTime(tz->toLocal(now), tm);
breakTime(tz->toLocal(month.lastMeterReadTime), last);
if(tm.Day != last.Day) {
return false;
}

View File

@@ -14,14 +14,18 @@ enum CosemType {
CosemTypeNull = 0x00,
CosemTypeArray = 0x01,
CosemTypeStructure = 0x02,
CosemTypeBoolean = 0x03,
CosemTypeOctetString = 0x09,
CosemTypeString = 0x0A,
CosemTypeDLongSigned = 0x05,
CosemTypeDLongUnsigned = 0x06,
CosemTypeSigned = 0x0F,
CosemTypeLongSigned = 0x10,
CosemTypeUnsigned = 0x11,
CosemTypeLongUnsigned = 0x12,
CosemTypeLong64Signed = 0x14,
CosemTypeLong64Unsigned = 0x15,
CosemTypeEnum = 0x16,
CosemTypeDateTime = 0x19
};

View File

@@ -24,7 +24,7 @@
#define DATA_PARSE_OK 0
#define DATA_PARSE_FAIL -1
#define DATA_PARSE_INCOMPLETE -2
#define DATA_PARSE_BOUNDRY_FLAG_MISSING -3
#define DATA_PARSE_BOUNDARY_FLAG_MISSING -3
#define DATA_PARSE_HEADER_CHECKSUM_ERROR -4
#define DATA_PARSE_FOOTER_CHECKSUM_ERROR -5
#define DATA_PARSE_INTERMEDIATE_SEGMENT -6

View File

@@ -9,15 +9,19 @@
#include "Arduino.h"
#include "DataParser.h"
#include "GcmParser.h"
class DSMRParser {
public:
int8_t parse(uint8_t *buf, DataParserContext &ctx, bool verified);
DSMRParser(GCMParser* gcmParser) { this->gcmParser = gcmParser; };
int8_t parse(uint8_t *buf, DataParserContext &ctx, bool verified, Print* debugger);
uint16_t getCrc();
uint16_t getCrcCalc();
private:
uint16_t crc;
uint16_t crc_calc;
GCMParser* gcmParser;
};
#endif

View File

@@ -18,7 +18,7 @@
class GCMParser {
public:
GCMParser(uint8_t *encryption_key, uint8_t *authentication_key);
int8_t parse(uint8_t *buf, DataParserContext &ctx);
int8_t parse(uint8_t *buf, DataParserContext &ctx, bool hastag = true);
private:
uint8_t encryption_key[16];
uint8_t authentication_key[16];

View File

@@ -12,6 +12,5 @@
uint16_t crc16(const uint8_t* p, int len);
uint16_t crc16_x25(const uint8_t* p, int len);
uint16_t crc16_1021(const uint8_t* p, int len);
#endif

View File

@@ -19,12 +19,10 @@ time_t decodeCosemDateTime(CosemDateTime timestamp) {
tm.Minute = timestamp.minute;
tm.Second = timestamp.second;
//Serial.printf("\nY: %d, M: %d, D: %d, h: %d, m: %d, s: %d, deviation: 0x%2X, status: 0x%1X\n", tm.Year, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second, timestamp.deviation, timestamp.status);
time_t time = makeTime(tm);
int16_t deviation = ntohs(timestamp.deviation);
if(deviation >= -720 && deviation <= 720) {
time -= deviation * 60;
time += deviation * 60;
}
return time;
}

View File

@@ -9,27 +9,77 @@
#include "hexutils.h"
#include "lwip/def.h"
int8_t DSMRParser::parse(uint8_t *buf, DataParserContext &ctx, bool verified) {
// verified indicates that this data was encapsulated in something else, so we know this has the correct size etc
int8_t DSMRParser::parse(uint8_t *buf, DataParserContext &ctx, bool verified, Print* debugger) {
uint16_t lenBefore = ctx.length;
uint16_t crcPos = 0;
bool reachedEnd = verified;
uint8_t lastByte = 0x00;
for(int pos = 0; pos < ctx.length; pos++) {
for(uint16_t pos = 0; pos < ctx.length; pos++) {
uint8_t b = *(buf+pos);
if(pos == 0 && b != '/') return DATA_PARSE_BOUNDRY_FLAG_MISSING;
if(pos > 0 && b == '!' && lastByte == '\n') crcPos = pos+1;
if(crcPos > 0 && b == '\n') reachedEnd = true;
if(pos == 0 && b != '/') return DATA_PARSE_BOUNDARY_FLAG_MISSING;
if(pos > 0 && b == '!') crcPos = pos+1;
if(crcPos > 0 && b == 0x0A && lastByte == 0x0D) {
reachedEnd = true;
ctx.length = pos;
break;
}
lastByte = b;
}
if(!reachedEnd) return DATA_PARSE_INCOMPLETE;
buf[ctx.length+1] = '\0';
if(crcPos > 0) {
// If we expect data to be encrypted and it was not previously verified, decrypt content
if(gcmParser != NULL && !verified) {
uint8_t* ptr = (uint8_t*) buf;
while(*ptr != 0x0D && *ptr != 0x0A) ptr++;
while(*ptr == 0x0D || *ptr == 0x0A) ptr++;
uint16_t pos = ptr-buf;
DataParserContext gcmCtx = {
DATA_TAG_GCM,
crcPos - pos - 1,
ctx.timestamp
};
if(debugger != NULL) {
debugger->printf_P(PSTR("DSMR wants to decrypt at position %lu, length: %d, payload:\n"), pos, gcmCtx.length);
debugPrint(ptr, 0, gcmCtx.length, debugger);
}
int8_t gcmRet = gcmParser->parse(ptr, gcmCtx, false);
if(gcmRet < 0) {
if(debugger != NULL) {
debugger->printf_P(PSTR(" - Failed! (%d)\n"), gcmRet);
}
return gcmRet;
} else {
if(debugger != NULL) {
debugger->printf_P(PSTR(" - Success! (%d)\n"), gcmRet);
}
ptr += gcmRet;
for(uint16_t i = 0; i < gcmCtx.length; i++) {
buf[pos++] = ptr[i];
}
ptr = buf + crcPos - 1;
crcPos = pos + 1;
while(*ptr != '\0') {
ctx.length = pos;
buf[pos++] = *(ptr++);
}
while(pos < lenBefore) {
buf[pos++] = '\0';
}
}
} else if(crcPos > 0) {
crc_calc = crc16(buf, crcPos);
crc = 0x0000;
fromHex((uint8_t*) &crc, String((char*) buf+crcPos), 2);
crc = ntohs(crc);
if(crc != crc_calc)
if(crc > 0 && crc != crc_calc) {
if(debugger != NULL) {
debugger->printf_P(PSTR("CRC incorrrect, %04X != %04X at position %lu\n"), crc, crc_calc, crcPos);
}
return DATA_PARSE_FOOTER_CHECKSUM_ERROR;
}
}
return DATA_PARSE_OK;
}

View File

@@ -11,7 +11,7 @@ int8_t GBTParser::parse(uint8_t *d, DataParserContext &ctx) {
GBTHeader* h = (GBTHeader*) (d);
uint16_t sequence = ntohs(h->sequence);
if(h->flag != GBT_TAG) return DATA_PARSE_BOUNDRY_FLAG_MISSING;
if(h->flag != GBT_TAG) return DATA_PARSE_BOUNDARY_FLAG_MISSING;
if(sequence == 1) {
if(buf == NULL) buf = (uint8_t *)malloc((size_t)1024); // TODO find out from first package ?

View File

@@ -17,25 +17,34 @@ GCMParser::GCMParser(uint8_t *encryption_key, uint8_t *authentication_key) {
memcpy(this->authentication_key, authentication_key, 16);
}
int8_t GCMParser::parse(uint8_t *d, DataParserContext &ctx) {
int8_t GCMParser::parse(uint8_t *d, DataParserContext &ctx, bool hastag) {
if(ctx.length < 12) return DATA_PARSE_INCOMPLETE;
uint32_t headersize = 0;
uint8_t* ptr = (uint8_t*) d;
if(*ptr != GCM_TAG) return DATA_PARSE_BOUNDRY_FLAG_MISSING;
ptr++;
if(hastag) {
if(*ptr != GCM_TAG) return DATA_PARSE_BOUNDARY_FLAG_MISSING;
ptr++;
headersize++;
}
// Encrypted APDU
// http://www.weigu.lu/tutorials/sensors2bus/04_encryption/index.html
uint8_t systemTitleLength = *ptr;
ptr++;
headersize++;
uint8_t initialization_vector[12];
memcpy(ctx.system_title, ptr, systemTitleLength);
memcpy(initialization_vector, ctx.system_title, systemTitleLength);
memset(ctx.system_title, 0, 8);
memset(initialization_vector, 0, 12);
if(systemTitleLength > 0) {
memcpy(ctx.system_title, ptr, systemTitleLength);
memcpy(initialization_vector, ctx.system_title, systemTitleLength);
ptr += systemTitleLength;
headersize += systemTitleLength;
}
uint32_t len = 0;
uint32_t headersize = 2 + systemTitleLength;
ptr += systemTitleLength;
if(((*ptr) & 0xFF) == 0x81) {
// 1-byte payload length
ptr++;

View File

@@ -29,10 +29,10 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
// First and last byte should be HDLC_FLAG
if(h->flag != HDLC_FLAG || f->flag != HDLC_FLAG)
return DATA_PARSE_BOUNDRY_FLAG_MISSING;
return DATA_PARSE_BOUNDARY_FLAG_MISSING;
// Verify FCS
if(ntohs(f->fcs) != crc16_x25(d + 1, len - sizeof *f - 1))
if(f->fcs > 0 && ntohs(f->fcs) != crc16_x25(d + 1, len - sizeof *f - 1))
return DATA_PARSE_FOOTER_CHECKSUM_ERROR;
// Skip destination address, LSB marks last byte
@@ -50,7 +50,7 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
HDLC3CtrlHcs* t3 = (HDLC3CtrlHcs*) (ptr);
// Verify HCS
if(ntohs(t3->hcs) != crc16_x25(d + 1, ptr-d))
if(t3->hcs > 0 && ntohs(t3->hcs) != crc16_x25(d + 1, ptr-d))
return DATA_PARSE_HEADER_CHECKSUM_ERROR;
ptr += 3;
@@ -69,7 +69,12 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
if(buf == NULL) return DATA_PARSE_FAIL;
memcpy(buf + pos, ptr+3, ctx.length); // +3 to skip LLC
if((*ptr) == DATA_TAG_LLC) {
ptr += 3; // Skip LLC
ctx.length -= 3;
}
memcpy(buf + pos, ptr, ctx.length);
pos += ctx.length;
lastSequenceNumber++;
@@ -78,7 +83,12 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
lastSequenceNumber = 0;
if(buf == NULL) return DATA_PARSE_FAIL;
memcpy(buf + pos, ptr+3, ctx.length); // +3 to skip LLC
if((*ptr) == DATA_TAG_LLC) {
ptr += 3; // Skip LLC
ctx.length -= 3;
}
memcpy(buf + pos, ptr, ctx.length);
pos += ctx.length;
memcpy((uint8_t *) d, buf, pos);

View File

@@ -19,7 +19,7 @@ int8_t MBUSParser::parse(uint8_t *d, DataParserContext &ctx) {
MbusHeader* mh = (MbusHeader*) d;
if(mh->flag1 != MBUS_START || mh->flag2 != MBUS_START)
return DATA_PARSE_BOUNDRY_FLAG_MISSING;
return DATA_PARSE_BOUNDARY_FLAG_MISSING;
// First two bytes is 1-byte length value repeated. Only used for last segment
if(mh->len1 != mh->len2)
@@ -40,7 +40,7 @@ int8_t MBUSParser::parse(uint8_t *d, DataParserContext &ctx) {
MbusFooter* mf = (MbusFooter*) (d + len + headersize);
if(mf->flag != MBUS_END)
return DATA_PARSE_BOUNDRY_FLAG_MISSING;
return DATA_PARSE_BOUNDARY_FLAG_MISSING;
if(checksum(d + headersize, len) != mf->fcs)
return DATA_PARSE_FOOTER_CHECKSUM_ERROR;

View File

@@ -33,22 +33,3 @@ uint16_t crc16 (const uint8_t *p, int len) {
return crc;
}
uint16_t crc16_1021(const uint8_t *p, int len) {
uint32_t crc = 0x0000;
for(int i = 0; i < len; i++) {
int mask = 0x80;
while(mask > 0) {
crc <<= 1;
if (p[i] & mask){
crc |= 1;
}
mask>>=1;
if (crc & 0x10000) {
crc &= 0xffff;
crc ^= 0x1021;
}
}
}
return crc;
}

View File

@@ -0,0 +1,151 @@
#pragma once
#include <stdint.h>
#include <Print.h>
#include "HwTools.h"
#include "AmsData.h"
#include "AmsConfiguration.h"
#if defined(ESP32)
#include "esp_flash_partitions.h"
#include "LittleFS.h"
#include "WiFi.h"
#include "HTTPClient.h"
#define AMS_PARTITION_TABLE_OFFSET 0x8000
#define AMS_PARTITION_APP0_OFFSET 0x10000
#define AMS_PARTITION_APP_SIZE 0x1D0000
#define AMS_PARTITION_MIN_SPIFFS_SIZE 0x20000
#elif defined(ESP8266)
#include <ESP8266HTTPClient.h>
#define AMS_FLASH_SKETCH_SIZE 0xFEFF0
#define AMS_FLASH_OTA_START AMS_FLASH_OTA_SIZE
#endif
#if defined(AMS_REMOTE_DEBUG)
#include "RemoteDebug.h"
#endif
#define AMS_UPDATE_ERR_OK 0
#define AMS_UPDATE_ERR_DETAILS 1
#define AMS_UPDATE_ERR_FETCH 2
#define AMS_UPDATE_ERR_ERASE 3
#define AMS_UPDATE_ERR_WRITE 4
#define AMS_UPDATE_ERR_READ 5
#define AMS_UPDATE_ERR_MD5 6
#define AMS_UPDATE_ERR_ACTIVATE 7
#define AMS_UPDATE_ERR_REBOOT 64
#define AMS_UPDATE_ERR_SUCCESS_SIGNAL 122
#define AMS_UPDATE_ERR_SUCCESS_CONFIRMED 123
#define UPDATE_BUF_SIZE 4096
class AmsFirmwareUpdater {
public:
#if defined(AMS_REMOTE_DEBUG)
AmsFirmwareUpdater(RemoteDebug* debugger, HwTools* hw, AmsData* meterState);
#else
AmsFirmwareUpdater(Print* debugger, HwTools* hw, AmsData* meterState);
#endif
bool relocateOrRepartitionIfNecessary();
void loop();
char* getNextVersion();
bool setTargetVersion(const char* version);
void getUpgradeInformation(UpgradeInformation&);
float getProgress();
bool activateDownloadedFirmware();
void setUpgradeInformation(UpgradeInformation&);
bool isUpgradeInformationChanged();
void ackUpgradeInformationChanged();
void setFirmwareChannel(uint8_t channel) {
if(firmwareChannel != channel) {
firmwareChannel = channel;
lastVersionCheck = 0;
}
}
bool startFirmwareUpload(uint32_t size, const char* version);
bool addFirmwareUploadChunk(uint8_t* buf, size_t length);
bool completeFirmwareUpload(uint32_t size);
private:
#if defined(ESP8266)
char chipType[10] = "esp8266";
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
char chipType[10] = "esp32s2";
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
char chipType[10] = "esp32s3";
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
char chipType[10] = "esp32c3";
#elif defined(ESP32)
#if defined(CONFIG_FREERTOS_UNICORE)
char chipType[10] = "esp32solo";
#else
char chipType[10] = "esp32";
#endif
#endif
#if defined(AMS_REMOTE_DEBUG)
RemoteDebug* debugger;
#else
Print* debugger;
#endif
HwTools* hw;
AmsData* meterState;
bool updateStatusChanged = false;
UpgradeInformation updateStatus = {"","",0,0,0,0,0};
uint16_t lastSaveBlocksWritten = 0;
String md5;
uint32_t lastVersionCheck = 0;
uint8_t firmwareChannel;
bool autoUpgrade;
char nextVersion[17];
void getChannelName(char * buffer);
bool fetchNextVersion();
bool fetchVersionDetails();
bool fetchFirmwareChunk(HTTPClient& http);
bool writeBufferToFlash();
bool verifyChecksum();
bool activateNewFirmware();
bool writeUpdateStatus();
bool isFlashReadyForNextUpdateVersion(uint32_t size);
uint8_t* buf = NULL;
uint16_t bufPos = 0;
#if defined(ESP32)
bool readPartition(uint8_t num, const esp_partition_info_t* info);
bool writePartition(uint8_t num, const esp_partition_info_t* info);
bool copyData(const esp_partition_info_t* src, esp_partition_info_t* dst, bool eraseFirst=true);
bool copyFile(fs::LittleFSFS* src, fs::LittleFSFS* dst, const char* filename);
uint8_t* extractFileData(const char* filename, size_t& size);
void saveFileData(const char* filename, uint8_t* data, size_t size);
bool relocateAppToFirst(const esp_partition_t* active);
bool findPartition(const char* label, const esp_partition_info_t* info);
bool hasLargeEnoughAppPartitions();
bool canMigratePartitionTable();
bool hasTwoSpiffs();
bool spiffsOnCorrectLocation();
bool hasFiles();
bool clearPartitionTable();
bool writeNewPartitionChecksum(uint8_t num);
bool writePartitionTableWithSpiffsAtOldAndApp1();
bool writePartitionTableWithSpiffsAtApp1AndNew();
bool writePartitionTableFinal();
bool moveLittleFsFromOldToApp1();
bool moveLittleFsFromApp1ToNew();
#elif defined(ESP8266)
uintptr_t getFirmwareUpdateStart();
#endif
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
#pragma once
#include "AmsDataStorage.h"
class AmsJsonGenerator {
public:
static void generateDayPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize);
static void generateMonthPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize);
};

View File

@@ -0,0 +1,17 @@
#include "AmsJsonGenerator.h"
void AmsJsonGenerator::generateDayPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize) {
uint16_t pos = snprintf_P(buf, bufSize, PSTR("{\"unit\":\"kwh\""));
for(uint8_t i = 0; i < 24; i++) {
pos += snprintf_P(buf+pos, bufSize-pos, PSTR(",\"i%02d\":%.3f,\"e%02d\":%.3f"), i, ds->getHourImport(i) / 1000.0, i, ds->getHourExport(i) / 1000.0);
}
snprintf_P(buf+pos, bufSize-pos, PSTR("}"));
}
void AmsJsonGenerator::generateMonthPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize) {
uint16_t pos = snprintf_P(buf, bufSize, PSTR("{\"unit\":\"kwh\""));
for(uint8_t i = 1; i < 32; i++) {
pos += snprintf_P(buf+pos, bufSize-pos, PSTR(",\"i%02d\":%.3f,\"e%02d\":%.3f"), i, ds->getDayImport(i) / 1000.0, i, ds->getDayExport(i) / 1000.0);
}
snprintf_P(buf+pos, bufSize-pos, PSTR("}"));
}

View File

@@ -14,6 +14,7 @@
#include "EnergyAccounting.h"
#include "HwTools.h"
#include "PriceService.h"
#include "AmsFirmwareUpdater.h"
#if defined(ESP32)
#include <esp_task_wdt.h>
@@ -21,33 +22,51 @@
class AmsMqttHandler {
public:
AmsMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf) {
#if defined(AMS_REMOTE_DEBUG)
AmsMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, AmsFirmwareUpdater* updater) {
#else
AmsMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, AmsFirmwareUpdater* updater) {
#endif
this->mqttConfig = mqttConfig;
this->mqttConfigChanged = true;
this->debugger = debugger;
this->json = buf;
this->updater = updater;
mqtt.dropOverflow(true);
pubTopic = String(mqttConfig.publishTopic);
subTopic = String(mqttConfig.subscribeTopic);
if(subTopic.isEmpty()) subTopic = pubTopic+"/command";
};
void setCaVerification(bool);
void setConfig(MqttConfig& mqttConfig);
bool connect();
bool defaultSubscribe();
void disconnect();
lwmqtt_err_t lastError();
bool connected();
bool loop();
bool isRebootSuggested();
virtual uint8_t getFormat() { return 0; };
virtual bool postConnect() { return false; };
virtual bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) { return false; };
virtual bool publishTemperatures(AmsConfiguration*, HwTools*) { return false; };
virtual bool publishPrices(PriceService* ps) { return false; };
virtual bool publishSystem(HwTools*, PriceService*, EnergyAccounting*) { return false; };
virtual bool publishRaw(String data) { return false; };
virtual bool publishRaw(uint8_t* raw, size_t length) { return false; };
virtual bool publishFirmware() { return false; };
virtual void onMessage(String &topic, String &payload) {};
virtual ~AmsMqttHandler() {
if(mqttSecureClient != NULL) {
mqttSecureClient->stop();
delete mqttSecureClient;
}
if(mqttClient != NULL) {
mqttClient->stop();
delete mqttClient;
@@ -55,7 +74,11 @@ public:
};
protected:
#if defined(AMS_REMOTE_DEBUG)
RemoteDebug* debugger;
#else
Stream* debugger;
#endif
MqttConfig mqttConfig;
bool mqttConfigChanged = true;
MQTTClient mqtt = MQTTClient(256);
@@ -63,8 +86,17 @@ protected:
bool caVerification = true;
WiFiClient *mqttClient = NULL;
WiFiClientSecure *mqttSecureClient = NULL;
boolean _connected = false;
char* json;
uint16_t BufferSize = 2048;
uint64_t lastStateUpdate = 0;
uint64_t lastSuccessfulLoop = 0;
String pubTopic;
String subTopic;
AmsFirmwareUpdater* updater = NULL;
bool rebootSuggested = false;
};
#endif

View File

@@ -8,6 +8,7 @@
#include "FirmwareVersion.h"
#include "AmsStorage.h"
#include "LittleFS.h"
#include "Uptime.h"
void AmsMqttHandler::setCaVerification(bool caVerification) {
this->caVerification = caVerification;
@@ -33,15 +34,18 @@ bool AmsMqttHandler::connect() {
if(epoch < FirmwareVersion::BuildEpoch) {
return false;
}
bool applySslConfiguration = mqttConfigChanged;
if(mqttSecureClient == NULL) {
mqttSecureClient = new WiFiClientSecure();
#if defined(ESP8266)
mqttSecureClient->setBufferSizes(512, 512);
return false;
#endif
applySslConfiguration = true;
}
if(mqttConfigChanged) {
if(applySslConfiguration) {
if(caVerification && LittleFS.begin()) {
File file;
@@ -99,7 +103,20 @@ bool AmsMqttHandler::connect() {
actualClient = mqttClient;
}
// This section helps with power saving on ESP32 devices by reducing timeouts
// The timeout is multiplied by 10 because WiFiClient is retrying 10 times internally
// Power drain for this timeout is too great when using the default 3s timeout
// On ESP8266 the timeout is used differently and the following code causes MQTT instability
#if defined(ESP32)
int clientTimeout = mqttConfig.timeout / 1000;
if(clientTimeout > 3) clientTimeout = 3; // 3000ms is default, see WiFiClient.cpp WIFI_CLIENT_DEF_CONN_TIMEOUT_MS
actualClient->setTimeout(clientTimeout);
// Why can't we set number of retries for write here? WiFiClient defaults to 10 (10*3s == 30s)
#endif
mqttConfigChanged = false;
mqtt.setTimeout(mqttConfig.timeout);
mqtt.setKeepAlive(mqttConfig.keepalive);
mqtt.begin(mqttConfig.host, mqttConfig.port, *actualClient);
String statusTopic = String(mqttConfig.publishTopic) + "/status";
mqtt.setWill(statusTopic.c_str(), "offline", true, 0);
@@ -114,19 +131,21 @@ bool AmsMqttHandler::connect() {
// Connect to a unsecure or secure MQTT server
if ((strlen(mqttConfig.username) == 0 && mqtt.connect(mqttConfig.clientId)) ||
(strlen(mqttConfig.username) > 0 && mqtt.connect(mqttConfig.clientId, mqttConfig.username, mqttConfig.password))) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Successfully connected to MQTT\n"));
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Successfully connected to MQTT\n"));
mqtt.onMessage(std::bind(&AmsMqttHandler::onMessage, this, std::placeholders::_1, std::placeholders::_2));
if(strlen(mqttConfig.subscribeTopic) > 0) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR(" Subscribing to [%s]\n"), mqttConfig.subscribeTopic);
if(!mqtt.subscribe(mqttConfig.subscribeTopic)) {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR(" Unable to subscribe to to [%s]\n"), mqttConfig.subscribeTopic);
}
}
mqtt.publish(statusTopic, "online", true, 0);
_connected = mqtt.publish(statusTopic, "online", true, 0);
mqtt.loop();
defaultSubscribe();
postConnect();
return true;
} else {
if (debugger->isActive(RemoteDebug::ERROR)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
{
debugger->printf_P(PSTR("Failed to connect to MQTT: %d\n"), mqtt.lastError());
#if defined(ESP8266)
if(mqttSecureClient) {
@@ -139,9 +158,29 @@ bool AmsMqttHandler::connect() {
}
}
bool AmsMqttHandler::defaultSubscribe() {
bool ret = true;
if(!subTopic.isEmpty()) {
if(mqtt.subscribe(subTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Subscribed to [%s]\n"), subTopic.c_str());
} else {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Unable to subscribe to [%s]\n"), subTopic.c_str());
ret = false;
}
}
return ret;
}
void AmsMqttHandler::disconnect() {
mqtt.disconnect();
mqtt.loop();
_connected = false;
delay(10);
yield();
}
@@ -151,12 +190,25 @@ lwmqtt_err_t AmsMqttHandler::lastError() {
}
bool AmsMqttHandler::connected() {
return mqtt.connected();
return _connected && mqtt.connected();
}
bool AmsMqttHandler::loop() {
bool ret = mqtt.loop();
delay(10);
uint64_t now = millis64();
bool ret = connected() && mqtt.loop();
if(ret) {
lastSuccessfulLoop = now;
} else if(mqttConfig.rebootMinutes > 0) {
if(now - lastSuccessfulLoop > (uint64_t) mqttConfig.rebootMinutes * 60000) {
// Reboot the device if the MQTT connection is lost for too long
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("MQTT connection lost for over %d minutes, rebooting device\n"), mqttConfig.rebootMinutes);
rebootSuggested = true;
}
}
delay(10); // Needed to preserve power. After adding this, the voltage is super smooth on a HAN powered device
yield();
#if defined(ESP32)
esp_task_wdt_reset();
@@ -164,4 +216,8 @@ bool AmsMqttHandler::loop() {
ESP.wdtFeed();
#endif
return ret;
}
bool AmsMqttHandler::isRebootSuggested() {
return rebootSuggested;
}

View File

@@ -7,7 +7,9 @@
#ifndef _CLOUDCONNECTOR_H
#define _CLOUDCONNECTOR_H
#if defined(AMS_REMOTE_DEBUG)
#include "RemoteDebug.h"
#endif
#include "mbedtls/ssl.h"
#include "mbedtls/platform.h"
#include "mbedtls/net.h"
@@ -35,7 +37,7 @@
#warning "Unsupported board type"
#endif
#define CC_BUF_SIZE 2048
#define CC_BUF_SIZE 4096
static const char CC_JSON_POWER[] PROGMEM = ",\"%s\":{\"P\":%lu,\"Q\":%lu}";
static const char CC_JSON_POWER_LIST3[] PROGMEM = ",\"%s\":{\"P\":%lu,\"Q\":%lu,\"tP\":%.3f,\"tQ\":%.3f}";
@@ -51,20 +53,31 @@ struct CloudData {
class CloudConnector {
public:
#if defined(AMS_REMOTE_DEBUG)
CloudConnector(RemoteDebug*);
bool setup(CloudConfig& config, MeterConfig& meter, SystemConfig& system, NtpConfig& ntp, HwTools* hw, ResetDataContainer* rdc);
#else
CloudConnector(Stream*);
#endif
bool setup(CloudConfig& config, MeterConfig& meter, SystemConfig& system, NtpConfig& ntp, HwTools* hw, ResetDataContainer* rdc, PriceService* ps);
void setMqttHandler(AmsMqttHandler* mqttHandler);
void update(AmsData& data, EnergyAccounting& ea);
void setPriceConfig(PriceServiceConfig&);
void setEnergyAccountingConfig(EnergyAccountingConfig&);
void forceUpdate();
void forcePriceUpdate();
void setConnectionHandler(ConnectionHandler* ch);
String generateSeed();
private:
#if defined(AMS_REMOTE_DEBUG)
RemoteDebug* debugger = NULL;
#else
Stream* debugger = NULL;
#endif
HwTools* hw = NULL;
ConnectionHandler* ch = NULL;
ResetDataContainer* rdc = NULL;
PriceService* ps = NULL;
AmsMqttHandler* mqttHandler = NULL;
CloudConfig config;
PriceServiceConfig priceConfig;
@@ -73,6 +86,7 @@ private:
unsigned long lastEac = 0;
HTTPClient http;
WiFiUDP udp;
WiFiClient tcp;
int maxPwr = 0;
uint8_t boardType = 0;
char timezone[32];
@@ -85,7 +99,10 @@ private:
char mac[18];
char apmac[18];
String seed = "";
char clearBuffer[CC_BUF_SIZE];
uint8_t* httpBuffer = NULL;
unsigned char encryptedBuffer[256];
mbedtls_rsa_context* rsa = nullptr;
mbedtls_ctr_drbg_context ctr_drbg;
@@ -93,7 +110,6 @@ private:
char* pers = "amsreader";
bool init();
void debugPrint(byte *buffer, int start, int length);
String meterManufacturer(uint8_t type) {
switch(type) {

View File

@@ -23,7 +23,11 @@
#include "esp32s3/rom/rtc.h"
#endif
#if defined(AMS_REMOTE_DEBUG)
CloudConnector::CloudConnector(RemoteDebug* debugger) {
#else
CloudConnector::CloudConnector(Stream* debugger) {
#endif
this->debugger = debugger;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
@@ -46,7 +50,7 @@ CloudConnector::CloudConnector(RemoteDebug* debugger) {
sprintf_P(this->apmac, PSTR("%02X:%02X:%02X:%02X:%02X:%02X"), apmac[0], apmac[1], apmac[2], apmac[3], apmac[4], apmac[5]);
}
bool CloudConnector::setup(CloudConfig& config, MeterConfig& meter, SystemConfig& system, NtpConfig& ntp, HwTools* hw, ResetDataContainer* rdc) {
bool CloudConnector::setup(CloudConfig& config, MeterConfig& meter, SystemConfig& system, NtpConfig& ntp, HwTools* hw, ResetDataContainer* rdc, PriceService* ps) {
bool ret = false;
#if defined(ESP32)
if(!ESPRandom::isValidV4Uuid(config.clientId)) {
@@ -59,6 +63,7 @@ bool CloudConnector::setup(CloudConfig& config, MeterConfig& meter, SystemConfig
this->config = config;
this->hw = hw;
this->rdc = rdc;
this->ps = ps;
this->boardType = system.boardType;
strcpy(this->timezone, ntp.timezone);
@@ -85,7 +90,10 @@ bool CloudConnector::init() {
strcpy_P(config.hostname, PSTR("cloud.amsleser.no"));
snprintf_P(clearBuffer, CC_BUF_SIZE, PSTR("http://%s/hub/cloud/public.key"), config.hostname);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(CloudConnector) Downloading public key from %s\n"), clearBuffer);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("(CloudConnector) Downloading public key from %s\n"), clearBuffer);
#if defined(ESP8266)
WiFiClient client;
client.setTimeout(5000);
@@ -108,14 +116,11 @@ bool CloudConnector::init() {
memset(clearBuffer, 0, CC_BUF_SIZE);
snprintf(clearBuffer, CC_BUF_SIZE, pub.c_str());
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Cloud public key:\n%s\n"), clearBuffer);
mbedtls_pk_context pk;
mbedtls_pk_init(&pk);
int error_code = 0;
if((error_code = mbedtls_pk_parse_public_key(&pk, (unsigned char*) clearBuffer, strlen((const char*) clearBuffer)+1)) == 0){
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("RSA public key OK\n"));
rsa = mbedtls_pk_rsa(pk);
mbedtls_ctr_drbg_init(&ctr_drbg);
mbedtls_entropy_init(&entropy);
@@ -124,18 +129,37 @@ bool CloudConnector::init() {
&entropy, (const unsigned char *) pers,
strlen(pers));
if(ret != 0) {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("mbedtls_ctr_drbg_seed return code: %d\n"), ret);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("mbedtls_ctr_drbg_seed return code: %d\n"), ret);
}
return ret == 0;
} else {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("RSA public key read error: ");
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf("RSA public key read error: ");
mbedtls_strerror(error_code, clearBuffer, CC_BUF_SIZE);
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("%s\n", clearBuffer);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf("%s\n", clearBuffer);
}
} else {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("(CloudConnector) Communication error, returned status: %d\n"), status);
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(http.errorToString(status).c_str());
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(http.getString().c_str());
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("(CloudConnector) Communication error, returned status: %d\n"), status);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf(http.errorToString(status).c_str());
debugger->println();
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf(http.getString().c_str());
http.end();
}
@@ -145,17 +169,44 @@ bool CloudConnector::init() {
}
void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
if(!config.enabled) return;
unsigned long now = millis();
if(now-lastUpdate < config.interval*1000) return;
if(!ESPRandom::isValidV4Uuid(config.clientId)) {
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf_P(PSTR("(CloudConnector) Client ID is not valid\n"));
if(now-lastUpdate < ((unsigned long)config.interval)*1000) {
return;
};
bool sendFirst = lastUpdate == 0;
lastUpdate = now;
if(config.enabled) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("(CloudConnector) Enabled\n"));
} else {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("(CloudConnector) Not enabled\n"));
return;
}
if(!ESPRandom::isValidV4Uuid(config.clientId)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("(CloudConnector) Client ID is not valid\n"));
return;
}
if(data.getListType() < 2) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("(CloudConnector) List type not enough data\n"));
return;
}
if(data.getListType() < 2) return;
if(!initialized && !init()) {
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf_P(PSTR("Unable to initialize cloud connector\n"));
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Unable to initialize cloud connector\n"));
return;
}
initialized = true;
@@ -165,8 +216,13 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
int pos = 0;
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR("{\"id\":\"%s\""), uuid.c_str());
if(!seed.isEmpty()) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"seed\":\"%s\""), seed.c_str());
}
if(lastUpdate == 0) {
bool sendData = true;
if(sendFirst) {
seed.clear();
if(mainFuse > 0 && distributionSystem > 0) {
int voltage = distributionSystem == 2 ? 400 : 230;
if(data.isThreePhase()) {
@@ -221,8 +277,62 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
dns2.toString().c_str()
);
} else if(lastPriceConfig == 0) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"price\":{\"area\":\"%s\",\"currency\":\"%s\"}"), priceConfig.area, priceConfig.currency);
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"price\":{\"area\":\"%s\",\"currency\":\"%s\",\"modifiers\":["), priceConfig.area, priceConfig.currency);
if(ps != NULL) {
std::vector<PriceConfig> pc = ps->getPriceConfig();
if(pc.size() > 0) {
for(uint8_t i = 0; i < pc.size(); i++) {
PriceConfig& p = pc.at(i);
String days;
for(uint8_t d = 0; d < 7; d++) {
if((p.days >> d) & 0x1 == 0x1) {
days += String(d, 10) + ",";
}
}
days = days.substring(0, days.length()-1);
String hours;
for(uint8_t h = 0; h < 24; h++) {
if((p.hours >> h) & 0x1 == 0x1) {
hours += String(h, 10) + ",";
}
}
hours = hours.substring(0, hours.length()-1);
char start[8];
memset(start, 0, 8);
if(p.start_dayofmonth > 0 && p.start_month > 0) {
snprintf_P(start, 8, PSTR("[%d,%d]"), p.start_month, p.start_dayofmonth);
} else {
strcpy_P(start, PSTR("null"));
}
char end[8];
memset(end, 0, 8);
if(p.end_dayofmonth > 0 && p.end_month > 0) {
snprintf_P(end, 8, PSTR("[%d,%d]"), p.end_month, p.end_dayofmonth);
} else {
strcpy_P(end, PSTR("null"));
}
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR("{\"type\":%d,\"name\":\"%s\",\"dir\":%d,\"days\":[%s],\"hours\":[%s],\"value\":%.4f,\"start\":%s,\"end\":%s}%s"),
p.type,
p.name,
p.direction,
days.c_str(),
hours.c_str(),
p.value / 10000.0,
start,
end,
i == pc.size()-1 ? "" : ","
);
}
}
}
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR("]}"));
lastPriceConfig = now;
sendData = false;
} else if(lastEac == 0) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"accounting\":{\"hours\":%d,\"thresholds\":[%d,%d,%d,%d,%d,%d,%d,%d,%d]}"),
eac.hours,
@@ -239,163 +349,253 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
lastEac = now;
}
float vcc = 0.0;
int rssi = 0;
float temperature = -127;
if(hw != NULL) {
vcc = hw->getVcc();
rssi = hw->getWifiRssi();
temperature = hw->getTemperature();
}
uint8_t espStatus;
#if defined(ESP8266)
if(vcc < 2.0) { // Voltage not correct, ESP would not run on this voltage
espStatus = 1;
} else if(vcc > 2.8 && vcc < 3.5) {
espStatus = 1;
} else if(vcc > 2.7 && vcc < 3.6) {
espStatus = 2;
} else {
espStatus = 3;
}
#elif defined(ESP32)
if(vcc < 2.0) { // Voltage not correct, ESP would not run on this voltage
espStatus = 1;
} else if(vcc > 3.1 && vcc < 3.5) {
espStatus = 1;
} else if(vcc > 3.0 && vcc < 3.6) {
espStatus = 2;
} else {
espStatus = 3;
}
#endif
uint8_t hanStatus;
if(data.getLastError() != 0) {
hanStatus = 3;
} else if(data.getLastUpdateMillis() == 0 && now < 30000) {
hanStatus = 0;
} else if(now - data.getLastUpdateMillis() < 15000) {
hanStatus = 1;
} else if(now - data.getLastUpdateMillis() < 30000) {
hanStatus = 2;
} else {
hanStatus = 3;
}
uint8_t wifiStatus;
if(rssi > -75) {
wifiStatus = 1;
} else if(rssi > -95) {
wifiStatus = 2;
} else {
wifiStatus = 3;
}
uint8_t mqttStatus;
if(mqttHandler == NULL) {
mqttStatus = 0;
} else if(mqttHandler->connected()) {
mqttStatus = 1;
} else if(mqttHandler->lastError() == 0) {
mqttStatus = 2;
} else {
mqttStatus = 3;
}
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"data\":{\"clock\":%lu,\"up\":%lu,\"lastUpdate\":%lu,\"est\":%s"),
(uint32_t) time(nullptr),
(uint32_t) (millis64()/1000),
(uint32_t) (data.getLastUpdateMillis()/1000),
data.isCounterEstimated() ? "true" : "false"
);
if(data.getListType() > 2) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_POWER_LIST3, "import", data.getActiveImportPower(), data.getReactiveImportPower(), data.getActiveImportCounter(), data.getReactiveImportCounter());
} else {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_POWER, "import", data.getActiveImportPower(), data.getReactiveImportPower());
}
if(data.getListType() > 2) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_POWER_LIST3, "export", data.getActiveExportPower(), data.getReactiveExportPower(), data.getActiveExportCounter(), data.getReactiveExportCounter());
} else {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_POWER, "export", data.getActiveExportPower(), data.getReactiveExportPower());
}
if(data.getListType() > 1) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"phases\":{"));
bool first = true;
if(data.getL1Voltage() > 0.0) {
if(data.getListType() > 3) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE_LIST4, first ? "" : ",", 1, data.getL1Voltage(), String(data.getL1Current(), 2).c_str(), data.getL1ActiveImportPower(), data.getL1ActiveExportPower(), data.getL1PowerFactor());
} else {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE, first ? "" : ",", 1, data.getL1Voltage(), String(data.getL1Current(), 2).c_str());
}
first = false;
if(sendData) {
float vcc = 0.0;
int rssi = 0;
float temperature = -127;
if(hw != NULL) {
vcc = hw->getVcc();
rssi = hw->getWifiRssi();
temperature = hw->getTemperature();
}
if(data.getL2Voltage() > 0.0) {
if(data.getListType() > 3) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE_LIST4, first ? "" : ",", 2, data.getL2Voltage(), String(data.getL2Current(), 2).c_str(), data.getL2ActiveImportPower(), data.getL2ActiveExportPower(), data.getL2PowerFactor());
} else {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE, first ? "" : ",", 2, data.getL2Voltage(), data.isL2currentMissing() ? "null" : String(data.getL2Current(), 2).c_str());
}
first = false;
uint8_t espStatus;
#if defined(ESP8266)
if(vcc < 2.0) { // Voltage not correct, ESP would not run on this voltage
espStatus = 1;
} else if(vcc > 2.8 && vcc < 3.5) {
espStatus = 1;
} else if(vcc > 2.7 && vcc < 3.6) {
espStatus = 2;
} else {
espStatus = 3;
}
if(data.getL3Voltage() > 0.0) {
if(data.getListType() > 3) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE_LIST4, first ? "" : ",", 3, data.getL3Voltage(), String(data.getL3Current(), 2).c_str(), data.getL3ActiveImportPower(), data.getL3ActiveExportPower(), data.getL3PowerFactor());
} else {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE, first ? "" : ",", 3, data.getL3Voltage(), String(data.getL3Current(), 2).c_str());
}
first = false;
#elif defined(ESP32)
if(vcc < 2.0) { // Voltage not correct, ESP would not run on this voltage
espStatus = 1;
} else if(vcc > 3.1 && vcc < 3.5) {
espStatus = 1;
} else if(vcc > 3.0 && vcc < 3.6) {
espStatus = 2;
} else {
espStatus = 3;
}
#endif
uint8_t hanStatus;
if(data.getLastError() != 0) {
hanStatus = 3;
} else if(data.getLastUpdateMillis() == 0 && now < 30000) {
hanStatus = 0;
} else if(now - data.getLastUpdateMillis() < 15000) {
hanStatus = 1;
} else if(now - data.getLastUpdateMillis() < 30000) {
hanStatus = 2;
} else {
hanStatus = 3;
}
uint8_t wifiStatus;
if(rssi > -75) {
wifiStatus = 1;
} else if(rssi > -95) {
wifiStatus = 2;
} else {
wifiStatus = 3;
}
uint8_t mqttStatus;
if(mqttHandler == NULL) {
mqttStatus = 0;
} else if(mqttHandler->connected()) {
mqttStatus = 1;
} else if(mqttHandler->lastError() == 0) {
mqttStatus = 2;
} else {
mqttStatus = 3;
}
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"data\":{\"clock\":%lu,\"up\":%lu,\"lastUpdate\":%lu,\"est\":%s"),
(uint32_t) time(nullptr),
(uint32_t) (millis64()/1000),
(uint32_t) (data.getLastUpdateMillis()/1000),
data.isCounterEstimated() ? "true" : "false"
);
if(data.getListType() > 2) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_POWER_LIST3, "import", data.getActiveImportPower(), data.getReactiveImportPower(), data.getActiveImportCounter(), data.getReactiveImportCounter());
} else {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_POWER, "import", data.getActiveImportPower(), data.getReactiveImportPower());
}
if(data.getListType() > 2) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_POWER_LIST3, "export", data.getActiveExportPower(), data.getReactiveExportPower(), data.getActiveExportCounter(), data.getReactiveExportCounter());
} else {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_POWER, "export", data.getActiveExportPower(), data.getReactiveExportPower());
}
if(data.getListType() > 1) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"phases\":{"));
bool first = true;
if(data.getL1Voltage() > 0.0) {
if(data.getListType() > 3) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE_LIST4, first ? "" : ",", 1, data.getL1Voltage(), String(data.getL1Current(), 2).c_str(), data.getL1ActiveImportPower(), data.getL1ActiveExportPower(), data.getL1PowerFactor());
} else {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE, first ? "" : ",", 1, data.getL1Voltage(), String(data.getL1Current(), 2).c_str());
}
first = false;
}
if(data.getL2Voltage() > 0.0) {
if(data.getListType() > 3) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE_LIST4, first ? "" : ",", 2, data.getL2Voltage(), String(data.getL2Current(), 2).c_str(), data.getL2ActiveImportPower(), data.getL2ActiveExportPower(), data.getL2PowerFactor());
} else {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE, first ? "" : ",", 2, data.getL2Voltage(), data.isL2currentMissing() ? "null" : String(data.getL2Current(), 2).c_str());
}
first = false;
}
if(data.getL3Voltage() > 0.0) {
if(data.getListType() > 3) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE_LIST4, first ? "" : ",", 3, data.getL3Voltage(), String(data.getL3Current(), 2).c_str(), data.getL3ActiveImportPower(), data.getL3ActiveExportPower(), data.getL3PowerFactor());
} else {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_PHASE, first ? "" : ",", 3, data.getL3Voltage(), String(data.getL3Current(), 2).c_str());
}
first = false;
}
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR("}"));
}
if(data.getListType() > 3) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"pf\":%.2f"), data.getPowerFactor());
}
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"realtime\":{\"import\":%.3f,\"export\":%.3f}"), ea.getUseThisHour(), ea.getProducedThisHour());
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"vcc\":%.2f,\"temp\":%.2f,\"rssi\":%d,\"free\":%d"), vcc, temperature, rssi, ESP.getFreeHeap());
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_STATUS,
espStatus, 0,
hanStatus, data.getLastError(),
wifiStatus, 0,
mqttStatus, mqttHandler == NULL ? 0 : mqttHandler->lastError()
);
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR("}"));
}
if(data.getListType() > 3) {
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"pf\":%.2f"), data.getPowerFactor());
}
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"realtime\":{\"import\":%.3f,\"export\":%.3f}"), ea.getUseThisHour(), ea.getProducedThisHour());
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"vcc\":%.2f,\"temp\":%.2f,\"rssi\":%d,\"free\":%d"), vcc, temperature, rssi, ESP.getFreeHeap());
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, CC_JSON_STATUS,
espStatus, 0,
hanStatus, data.getLastError(),
wifiStatus, 0,
mqttStatus, mqttHandler == NULL ? 0 : mqttHandler->lastError()
);
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR("}"));
uint16_t crc = crc16((uint8_t*) clearBuffer, pos);
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"crc\":\"%04X\"}"), crc);
if(rsa == nullptr) return;
if(rsa == nullptr) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("RSA is null\n"));
return;
}
int ret = mbedtls_rsa_check_pubkey(rsa);
if(ret != 0) {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("mbedtls_rsa_pkcs1_encrypt return code: %d\n"), ret);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("mbedtls_rsa_pkcs1_encrypt return code: %d\n"), ret);
mbedtls_strerror(ret, clearBuffer, CC_BUF_SIZE);
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("%s\n"), clearBuffer);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("%s\n"), clearBuffer);
return;
}
memset(encryptedBuffer, 0, rsa->len);
int maxlen = 100 * (rsa->len/128);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(CloudConnector) Sending %d bytes and maxlen %d\n", pos, maxlen);
udp.beginPacket(config.hostname,7443);
for(int i = 0; i < pos; i += maxlen) {
int size = min(maxlen, pos-i);
int ret = mbedtls_rsa_pkcs1_encrypt(rsa, mbedtls_ctr_drbg_random, &ctr_drbg, MBEDTLS_RSA_PUBLIC, size, (unsigned char*) (clearBuffer+i), encryptedBuffer);
if(ret == 0) {
udp.write(encryptedBuffer, rsa->len);
delay(1);
} else {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("mbedtls_rsa_pkcs1_encrypt return code: %d\n"), ret);
mbedtls_strerror(ret, clearBuffer, CC_BUF_SIZE);
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("%s\n"), clearBuffer);
int maxlen = rsa->len - 11; // 11 should be the correct padding size for PKCS1
Stream *stream = NULL;
if(config.proto == 0) {
udp.beginPacket(config.hostname, config.port);
stream = &udp;
} else if(config.proto == 1) {
if(!tcp.connected()) {
int ret = tcp.connect(config.hostname, config.port, 1000);
if(ret != 1) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("tcp.connect(%s, %d) return code: %d\n"), config.hostname, config.port, ret);
return;
}
tcp.setTimeout((config.interval * 1000) / 2);
}
while(tcp.available()) tcp.read(); // Empty incoming buffer
stream = &tcp;
} else if(config.proto == 2) {
if(!http.connected()) {
http.setReuse(true);
snprintf_P(clearBuffer, CC_BUF_SIZE, PSTR("http://%s/hub/cloud/data"), config.hostname);
if(!http.begin(clearBuffer)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("(CloudConnector) Unable to start HTTP connector\n"));
http.end();
return;
}
}
if(httpBuffer == NULL) {
httpBuffer = (uint8_t*) malloc(CC_BUF_SIZE);
}
}
udp.endPacket();
lastUpdate = now;
int sendBytes = 0;
for(int i = 0; i < pos; i += maxlen) {
int ret = mbedtls_rsa_pkcs1_encrypt(rsa, mbedtls_ctr_drbg_random, &ctr_drbg, MBEDTLS_RSA_PUBLIC, maxlen, (unsigned char*) (clearBuffer+i), encryptedBuffer);
if(ret == 0) {
if(stream != NULL) {
stream->write(encryptedBuffer, rsa->len);
stream->flush();
} else {
memcpy(httpBuffer + sendBytes, encryptedBuffer, rsa->len);
}
sendBytes += rsa->len;
delay(1);
} else {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("mbedtls_rsa_pkcs1_encrypt return code: %d\n"), ret);
mbedtls_strerror(ret, clearBuffer, CC_BUF_SIZE);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("%s\n"), clearBuffer);
}
}
if(config.proto == 0) {
udp.endPacket();
} else if(config.proto == 1) {
tcp.write("\r\n");
tcp.flush();
} else if(config.proto == 2) {
http.addHeader("Content-Type", "application/octet-stream");
int status = http.POST(httpBuffer, sendBytes);
if(status != 200) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("(CloudConnector) Communication error 2, returned status: %d\n"), status);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf(http.errorToString(status).c_str());
debugger->println();
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf(http.getString().c_str());
http.end();
}
}
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("(CloudConnector) %d bytes sent to %s:%d from %s\n"), sendBytes, config.hostname, config.proto == 2 ? 80 : config.port, uuid.c_str());
}
void CloudConnector::forceUpdate() {
@@ -404,6 +604,10 @@ void CloudConnector::forceUpdate() {
lastEac = 0;
}
void CloudConnector::forcePriceUpdate() {
lastPriceConfig = 0;
}
void CloudConnector::setConnectionHandler(ConnectionHandler* ch) {
this->ch = ch;
}
@@ -418,18 +622,9 @@ void CloudConnector::setEnergyAccountingConfig(EnergyAccountingConfig& eac) {
this->lastEac = 0;
}
void CloudConnector::debugPrint(byte *buffer, int start, int length) {
for (int i = start; i < start + length; i++) {
if (buffer[i] < 0x10)
debugger->print(F("0"));
debugger->print(buffer[i], HEX);
debugger->print(F(" "));
if ((i - start + 1) % 16 == 0)
debugger->println(F(""));
else if ((i - start + 1) % 4 == 0)
debugger->print(F(" "));
yield(); // Let other get some resources too
}
debugger->println(F(""));
String CloudConnector::generateSeed() {
uint8_t key[16];
ESPRandom::uuid4(key);
seed = ESPRandom::uuidToString(key);
return seed;
}

View File

@@ -9,13 +9,19 @@
#include "ConnectionHandler.h"
#include <Arduino.h>
#if defined(AMS_REMOTE_DEBUG)
#include "RemoteDebug.h"
#endif
#define CONNECTION_TIMEOUT 30000
class EthernetConnectionHandler : public ConnectionHandler {
public:
#if defined(AMS_REMOTE_DEBUG)
EthernetConnectionHandler(RemoteDebug* debugger);
#else
EthernetConnectionHandler(Stream* debugger);
#endif
bool connect(NetworkConfig config, SystemConfig sys);
void disconnect(unsigned long reconnectDelay);
@@ -33,7 +39,11 @@ public:
#endif
private:
#if defined(AMS_REMOTE_DEBUG)
RemoteDebug* debugger;
#else
Stream* debugger;
#endif
NetworkConfig config;
bool connected = false;

View File

@@ -9,12 +9,18 @@
#include "ConnectionHandler.h"
#include <Arduino.h>
#if defined(AMS_REMOTE_DEBUG)
#include "RemoteDebug.h"
#endif
#include <DNSServer.h>
class WiFiAccessPointConnectionHandler : public ConnectionHandler {
public:
#if defined(AMS_REMOTE_DEBUG)
WiFiAccessPointConnectionHandler(RemoteDebug* debugger);
#else
WiFiAccessPointConnectionHandler(Stream* debugger);
#endif
bool connect(NetworkConfig config, SystemConfig sys);
void disconnect(unsigned long reconnectDelay);
@@ -32,7 +38,11 @@ public:
#endif
private:
#if defined(AMS_REMOTE_DEBUG)
RemoteDebug* debugger;
#else
Stream* debugger;
#endif
NetworkConfig config;
DNSServer dnsServer;

View File

@@ -9,7 +9,9 @@
#include "ConnectionHandler.h"
#include <Arduino.h>
#if defined(AMS_REMOTE_DEBUG)
#include "RemoteDebug.h"
#endif
#define CONNECTION_TIMEOUT 30000
#define RECONNECT_TIMEOUT 5000
@@ -20,7 +22,11 @@ esp_err_t set_esp_interface_ip(esp_interface_t interface, IPAddress local_ip=INA
class WiFiClientConnectionHandler : public ConnectionHandler {
public:
#if defined(AMS_REMOTE_DEBUG)
WiFiClientConnectionHandler(RemoteDebug* debugger);
#else
WiFiClientConnectionHandler(Stream* debugger);
#endif
bool connect(NetworkConfig config, SystemConfig sys);
void disconnect(unsigned long reconnectDelay);
@@ -38,7 +44,11 @@ public:
#endif
private:
#if defined(AMS_REMOTE_DEBUG)
RemoteDebug* debugger;
#else
Stream* debugger;
#endif
NetworkConfig config;
bool busPowered = false;
bool firstConnect = true;

View File

@@ -12,7 +12,11 @@
#include <lwip/dns.h>
#endif
#if defined(AMS_REMOTE_DEBUG)
EthernetConnectionHandler::EthernetConnectionHandler(RemoteDebug* debugger) {
#else
EthernetConnectionHandler::EthernetConnectionHandler(Stream* debugger) {
#endif
this->debugger = debugger;
this->mode = NETWORK_MODE_ETH_CLIENT;
}
@@ -56,11 +60,21 @@ bool EthernetConnectionHandler::connect(NetworkConfig config, SystemConfig sys)
ethPowerPin = 16;
ethMdc = 23;
ethMdio = 18;
} else if (sys.boardType == 245) { // wESP32
ethType = ETH_PHY_RTL8201;
ethEnablePin = -1;
ethAddr = 0;
ethClkMode = ETH_CLOCK_GPIO0_IN;
ethPowerPin = -1;
ethMdc = 16;
ethMdio = 17;
} else if(sys.boardType == 244) {
if (debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("LilyGO T-ETH-Lite\n"));
return false; // TODO
} else {
if (debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("Board type %d incompatible with ETH\n"), sys.boardType);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("Board type %d incompatible with ETH\n"), sys.boardType);
return false;
}
@@ -69,7 +83,10 @@ bool EthernetConnectionHandler::connect(NetworkConfig config, SystemConfig sys)
digitalWrite(ethEnablePin, 1);
}
if (debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Connecting to Ethernet\n"));
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Connecting to Ethernet\n"));
if(ETH.begin(ethAddr, ethPowerPin, ethMdc, ethMdio, ethType, ethClkMode)) {
#if defined(ESP32)
@@ -97,8 +114,12 @@ bool EthernetConnectionHandler::connect(NetworkConfig config, SystemConfig sys)
debugger->printf_P(PSTR("Static IP configuration is invalid, not using\n"));
}
}
this->config = config;
} else {
if (debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("Unable to start Ethernet\n"));
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("Unable to start Ethernet\n"));
}
}
#endif
@@ -106,7 +127,10 @@ bool EthernetConnectionHandler::connect(NetworkConfig config, SystemConfig sys)
}
void EthernetConnectionHandler::disconnect(unsigned long reconnectDelay) {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("Disconnecting!\n"));
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("Disconnecting!\n"));
}
bool EthernetConnectionHandler::isConnected() {
@@ -118,22 +142,38 @@ void EthernetConnectionHandler::eventHandler(WiFiEvent_t event, WiFiEventInfo_t
switch(event) {
case ARDUINO_EVENT_ETH_CONNECTED:
connected = true;
if(debugger->isActive(RemoteDebug::INFO)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
{
debugger->printf_P(PSTR("Successfully connected to Ethernet!\n"));
}
if(config.ipv6 && !ETH.enableIpV6()) {
debugger->printf_P(PSTR("Unable to enable IPv6\n"));
}
break;
case ARDUINO_EVENT_ETH_GOT_IP:
if(debugger->isActive(RemoteDebug::INFO)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
{
debugger->printf_P(PSTR("IP: %s\n"), getIP().toString().c_str());
debugger->printf_P(PSTR("GW: %s\n"), getGateway().toString().c_str());
for(uint8_t i = 0; i < 3; i++) {
IPAddress dns4 = getDns(i);
if(!dns4.isAny()) debugger->printf_P(PSTR("DNS: %s\n"), dns4.toString().c_str());
if(dns4 == IPAddress()) {
// No IP
} else {
debugger->printf_P(PSTR("DNS: %s\n"), dns4.toString().c_str());
}
}
}
break;
case ARDUINO_EVENT_ETH_GOT_IP6: {
if(debugger->isActive(RemoteDebug::INFO)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
{
IPv6Address ipv6 = getIPv6();
if(ipv6 == IPv6Address()) {
// No IP
@@ -154,7 +194,10 @@ void EthernetConnectionHandler::eventHandler(WiFiEvent_t event, WiFiEventInfo_t
}
case ARDUINO_EVENT_ETH_DISCONNECTED:
connected = false;
if(debugger->isActive(RemoteDebug::WARNING)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
{
debugger->printf_P(PSTR("Ethernet was disconnected!\n"));
}
break;

View File

@@ -6,7 +6,11 @@
#include "WiFiAccessPointConnectionHandler.h"
#if defined(AMS_REMOTE_DEBUG)
WiFiAccessPointConnectionHandler::WiFiAccessPointConnectionHandler(RemoteDebug* debugger) {
#else
WiFiAccessPointConnectionHandler::WiFiAccessPointConnectionHandler(Stream* debugger) {
#endif
this->debugger = debugger;
this->mode = NETWORK_MODE_WIFI_AP;
}
@@ -42,22 +46,37 @@ void WiFiAccessPointConnectionHandler::eventHandler(WiFiEvent_t event, WiFiEvent
IPAddress stationIP;
switch(event) {
case ARDUINO_EVENT_WIFI_AP_START:
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("WiFi access point started with SSID %s\n"), config.ssid);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("WiFi access point started with SSID %s\n"), config.ssid);
break;
case ARDUINO_EVENT_WIFI_AP_STOP:
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("WiFi access point stopped!\n"));
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("WiFi access point stopped!\n"));
break;
case ARDUINO_EVENT_WIFI_AP_STACONNECTED:
memcpy(mac, info.wifi_ap_staconnected.mac, 6);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Client connected to AP, client MAC: %02x:%02x:%02x:%02x:%02x:%02x\n"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Client connected to AP, client MAC: %02x:%02x:%02x:%02x:%02x:%02x\n"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
break;
case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED:
memcpy(mac, info.wifi_ap_staconnected.mac, 6);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Client disconnected from AP, client MAC: %02x:%02x:%02x:%02x:%02x:%02x\n"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Client disconnected from AP, client MAC: %02x:%02x:%02x:%02x:%02x:%02x\n"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
break;
case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED:
stationIP = info.wifi_ap_staipassigned.ip.addr;
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Client was assigned IP %s\n"), stationIP.toString().c_str());
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Client was assigned IP %s\n"), stationIP.toString().c_str());
break;
}
}

View File

@@ -10,7 +10,11 @@
#include <lwip/dns.h>
#endif
#if defined(AMS_REMOTE_DEBUG)
WiFiClientConnectionHandler::WiFiClientConnectionHandler(RemoteDebug* debugger) {
#else
WiFiClientConnectionHandler::WiFiClientConnectionHandler(Stream* debugger) {
#endif
this->debugger = debugger;
this->mode = NETWORK_MODE_WIFI_CLIENT;
}
@@ -28,14 +32,20 @@ bool WiFiClientConnectionHandler::connect(NetworkConfig config, SystemConfig sys
}
if(WiFi.getMode() != WIFI_OFF) {
if (debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Not connected to WiFi, closing resources\n"));
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Not connected to WiFi, closing resources\n"));
disconnect(RECONNECT_TIMEOUT);
return false;
}
timeout = CONNECTION_TIMEOUT;
if (debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Connecting to WiFi network: %s\n"), config.ssid);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Connecting to WiFi network: %s\n"), config.ssid);
switch(sys.boardType) {
case 2: // spenceme
case 3: // Pow-K UART0
@@ -111,7 +121,10 @@ bool WiFiClientConnectionHandler::connect(NetworkConfig config, SystemConfig sys
}
yield();
} else {
if (debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("Unable to start WiFi\n"));
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("Unable to start WiFi\n"));
}
return true;
}
@@ -208,7 +221,10 @@ void WiFiClientConnectionHandler::wifi_sta_config(wifi_config_t * wifi_config, c
#endif
void WiFiClientConnectionHandler::disconnect(unsigned long reconnectDelay) {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf_P(PSTR("Disconnecting!\n"));
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("Disconnecting!\n"));
#if defined(ESP8266)
WiFiClient::stopAll();
#endif
@@ -235,7 +251,10 @@ void WiFiClientConnectionHandler::eventHandler(WiFiEvent_t event, WiFiEventInfo_
}
break;
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Successfully connected to WiFi!\n"));
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Successfully connected to WiFi!\n"));
if(config.ipv6 && !WiFi.enableIpV6()) {
debugger->printf_P(PSTR("Unable to enable IPv6\n"));
}
@@ -246,7 +265,10 @@ void WiFiClientConnectionHandler::eventHandler(WiFiEvent_t event, WiFiEventInfo_
wifi_phy_mode_t phyMode;
if(esp_wifi_sta_get_negotiated_phymode(&phyMode) == ESP_OK) {
if(phyMode > WIFI_PHY_MODE_11B) {
if (debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("WiFi supports better rates than 802.11b, disabling\n"));
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("WiFi supports better rates than 802.11b, disabling\n"));
config.use11b = false;
configChanged = true;
return;
@@ -284,18 +306,28 @@ void WiFiClientConnectionHandler::eventHandler(WiFiEvent_t event, WiFiEventInfo_
#endif
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP: {
if(debugger->isActive(RemoteDebug::INFO)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
{
debugger->printf_P(PSTR("IP: %s\n"), getIP().toString().c_str());
debugger->printf_P(PSTR("GW: %s\n"), getGateway().toString().c_str());
for(uint8_t i = 0; i < 3; i++) {
IPAddress dns4 = getDns(i);
if(!dns4.isAny()) debugger->printf_P(PSTR("DNS: %s\n"), dns4.toString().c_str());
if(dns4 == IPAddress()) {
// No IP
} else {
debugger->printf_P(PSTR("DNS: %s\n"), dns4.toString().c_str());
}
}
}
break;
}
case ARDUINO_EVENT_WIFI_STA_GOT_IP6: {
if(debugger->isActive(RemoteDebug::INFO)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
{
IPv6Address ipv6 = getIPv6();
if(ipv6 == IPv6Address()) {
// No IP
@@ -322,7 +354,10 @@ void WiFiClientConnectionHandler::eventHandler(WiFiEvent_t event, WiFiEventInfo_
break;
default:
if(strlen(descr) > 0) {
if(debugger->isActive(RemoteDebug::WARNING)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
{
debugger->printf_P(PSTR("WiFi disconnected, reason %s\n"), descr);
}
disconnect(RECONNECT_TIMEOUT);

View File

@@ -12,19 +12,29 @@
class DomoticzMqttHandler : public AmsMqttHandler {
public:
DomoticzMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, DomoticzConfig config) : AmsMqttHandler(mqttConfig, debugger, buf) {
#if defined(AMS_REMOTE_DEBUG)
DomoticzMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, DomoticzConfig config, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
this->config = config;
};
#else
DomoticzMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, DomoticzConfig config, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
this->config = config;
};
#endif
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*);
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
bool publishRaw(String data);
bool publishRaw(uint8_t* raw, size_t length);
void onMessage(String &topic, String &payload);
uint8_t getFormat();
void setDomoticzConfig(DomoticzConfig config) {
this->config = config;
}
private:
DomoticzConfig config;
double energy = 0.0;

View File

@@ -6,16 +6,29 @@
#include "DomoticzMqttHandler.h"
#include "json/domoticz_json.h"
#include "Uptime.h"
bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
bool DomoticzMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
bool ret = false;
AmsData data;
if(mqttConfig.stateUpdate) {
uint64_t now = millis64();
if(now-lastStateUpdate < mqttConfig.stateUpdateInterval * 1000) return false;
data.apply(*previousState);
data.apply(*update);
lastStateUpdate = now;
} else {
data = *update;
}
if (config.elidx > 0) {
if(data->getActiveImportCounter() > 1.0) {
energy = data->getActiveImportCounter();
if(data.getActiveImportCounter() > 1.0 && !data.isCounterEstimated()) {
energy = data.getActiveImportCounter();
}
if(energy > 0.0) {
char val[16];
snprintf_P(val, 16, PSTR("%.1f;%.1f"), (data->getActiveImportPower()/1.0), energy*1000.0);
snprintf_P(val, 16, PSTR("%.1f;%.1f"), (data.getActiveImportPower()/1.0), energy*1000.0);
snprintf_P(json, BufferSize, DOMOTICZ_JSON,
config.elidx,
val
@@ -25,12 +38,12 @@ bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyA
}
}
if(data->getListType() == 1)
if(data.getListType() == 1)
return ret;
if (config.vl1idx > 0){
char val[16];
snprintf_P(val, 16, PSTR("%.2f"), data->getL1Voltage());
snprintf_P(val, 16, PSTR("%.2f"), data.getL1Voltage());
snprintf_P(json, BufferSize, DOMOTICZ_JSON,
config.vl1idx,
val
@@ -41,7 +54,7 @@ bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyA
if (config.vl2idx > 0){
char val[16];
snprintf_P(val, 16, PSTR("%.2f"), data->getL2Voltage());
snprintf_P(val, 16, PSTR("%.2f"), data.getL2Voltage());
snprintf_P(json, BufferSize, DOMOTICZ_JSON,
config.vl2idx,
val
@@ -52,7 +65,7 @@ bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyA
if (config.vl3idx > 0){
char val[16];
snprintf(val, 16, "%.2f", data->getL3Voltage());
snprintf(val, 16, "%.2f", data.getL3Voltage());
snprintf_P(json, BufferSize, DOMOTICZ_JSON,
config.vl3idx,
val
@@ -63,7 +76,7 @@ bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyA
if (config.cl1idx > 0){
char val[16];
snprintf(val, 16, "%.1f;%.1f;%.1f", data->getL1Current(), data->getL2Current(), data->getL3Current());
snprintf(val, 16, "%.1f;%.1f;%.1f", data.getL1Current(), data.getL2Current(), data.getL3Current());
snprintf_P(json, BufferSize, DOMOTICZ_JSON,
config.cl1idx,
val
@@ -90,7 +103,7 @@ uint8_t DomoticzMqttHandler::getFormat() {
return 3;
}
bool DomoticzMqttHandler::publishRaw(String data) {
bool DomoticzMqttHandler::publishRaw(uint8_t* raw, size_t length) {
return false;
}

View File

@@ -13,11 +13,35 @@
#include "PriceService.h"
struct EnergyAccountingPeak {
uint8_t day;
uint8_t hour;
uint16_t value;
};
struct EnergyAccountingPeak6 {
uint8_t day;
uint16_t value;
};
struct EnergyAccountingData {
uint8_t version;
uint8_t month;
int32_t costToday;
int32_t costYesterday;
int32_t costThisMonth;
int32_t costLastMonth;
int32_t incomeToday;
int32_t incomeYesterday;
int32_t incomeThisMonth;
int32_t incomeLastMonth;
uint32_t lastMonthImport;
uint32_t lastMonthExport;
uint8_t lastMonthAccuracy;
EnergyAccountingPeak peaks[5];
time_t lastUpdated;
};
struct EnergyAccountingData6 {
uint8_t version;
uint8_t month;
int32_t costYesterday;
@@ -29,37 +53,7 @@ struct EnergyAccountingData {
uint32_t lastMonthImport;
uint32_t lastMonthExport;
uint8_t lastMonthAccuracy;
EnergyAccountingPeak peaks[5];
};
struct EnergyAccountingData5 {
uint8_t version;
uint8_t month;
uint16_t costYesterday;
uint16_t costThisMonth;
uint16_t costLastMonth;
uint16_t incomeYesterday;
uint16_t incomeThisMonth;
uint16_t incomeLastMonth;
EnergyAccountingPeak peaks[5];
};
struct EnergyAccountingData4 {
uint8_t version;
uint8_t month;
uint16_t costYesterday;
uint16_t costThisMonth;
uint16_t costLastMonth;
EnergyAccountingPeak peaks[5];
};
struct EnergyAccountingData2 {
uint8_t version;
uint8_t month;
uint16_t maxHour;
uint16_t costYesterday;
uint16_t costThisMonth;
uint16_t costLastMonth;
EnergyAccountingPeak6 peaks[5];
};
struct EnergyAccountingRealtimeData {
@@ -80,7 +74,11 @@ struct EnergyAccountingRealtimeData {
class EnergyAccounting {
public:
#if defined(AMS_REMOTE_DEBUG)
EnergyAccounting(RemoteDebug*, EnergyAccountingRealtimeData*);
#else
EnergyAccounting(Stream*, EnergyAccountingRealtimeData*);
#endif
void setup(AmsDataStorage *ds, EnergyAccountingConfig *config);
void setPriceService(PriceService *ps);
void setTimezone(Timezone*);
@@ -120,21 +118,24 @@ public:
void setData(EnergyAccountingData&);
void setCurrency(String currency);
float getPriceForHour(uint8_t d, uint8_t h);
private:
#if defined(AMS_REMOTE_DEBUG)
RemoteDebug* debugger = NULL;
#else
Stream* debugger = NULL;
#endif
bool init = false, initPrice = false;
AmsDataStorage *ds = NULL;
PriceService *ps = NULL;
EnergyAccountingConfig *config = NULL;
Timezone *tz = NULL;
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
EnergyAccountingRealtimeData* realtimeData = NULL;
String currency = "";
void calcDayCost();
bool updateMax(uint16_t val, uint8_t day);
bool updateMax(uint16_t val, uint8_t day, uint8_t hour);
};
#endif

View File

@@ -9,7 +9,11 @@
#include "AmsStorage.h"
#include "FirmwareVersion.h"
#if defined(AMS_REMOTE_DEBUG)
EnergyAccounting::EnergyAccounting(RemoteDebug* debugger, EnergyAccountingRealtimeData* rtd) {
#else
EnergyAccounting::EnergyAccounting(Stream* Stream, EnergyAccountingRealtimeData* rtd) {
#endif
data.version = 1;
this->debugger = debugger;
if(rtd->magic != 0x6A) {
@@ -26,7 +30,7 @@ EnergyAccounting::EnergyAccounting(RemoteDebug* debugger, EnergyAccountingRealti
rtd->lastImportUpdateMillis = 0;
rtd->lastExportUpdateMillis = 0;
}
this->realtimeData = rtd;
realtimeData = rtd;
}
void EnergyAccounting::setup(AmsDataStorage *ds, EnergyAccountingConfig *config) {
@@ -63,59 +67,55 @@ bool EnergyAccounting::update(AmsData* amsData) {
breakTime(tz->toLocal(now), local);
if(!init) {
this->realtimeData->lastImportUpdateMillis = 0;
this->realtimeData->lastExportUpdateMillis = 0;
this->realtimeData->currentHour = local.Hour;
this->realtimeData->currentDay = local.Day;
realtimeData->lastImportUpdateMillis = 0;
realtimeData->lastExportUpdateMillis = 0;
realtimeData->currentHour = local.Hour;
realtimeData->currentDay = local.Day;
if(!load()) {
data = { 6, local.Month,
0, 0, 0, // Cost
0, 0, 0, // Income
data = { 7, local.Month,
0, 0, 0, 0, // Cost
0, 0, 0, 0, // Income
0, 0, 0, // Last month import, export and accuracy
0, 0, // Peak 1
0, 0, // Peak 2
0, 0, // Peak 3
0, 0, // Peak 4
0, 0 // Peak 5
0, 0, 0, // Peak 1
0, 0, 0, // Peak 2
0, 0, 0, // Peak 3
0, 0, 0, // Peak 4
0, 0, 0 // Peak 5
};
}
init = true;
}
float price = getPriceForHour(PRICE_DIRECTION_IMPORT, 0);
if(!initPrice && price != PRICE_NO_VALUE) {
if(!initPrice && ps != NULL && ps->hasPrice()) {
calcDayCost();
}
if(local.Hour != this->realtimeData->currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) {
if(local.Hour != realtimeData->currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) {
tmElements_t oneHrAgo, oneHrAgoLocal;
breakTime(now-3600, oneHrAgo);
uint16_t val = round(ds->getHourImport(oneHrAgo.Hour) / 10.0);
breakTime(tz->toLocal(now-3600), oneHrAgoLocal);
ret |= updateMax(val, oneHrAgoLocal.Day);
ret |= updateMax(val, oneHrAgoLocal.Day, oneHrAgoLocal.Hour);
this->realtimeData->currentHour = local.Hour; // Need to be defined here so that day cost is correctly calculated
if(local.Hour > 0) {
calcDayCost();
}
realtimeData->currentHour = local.Hour; // Need to be defined here so that day cost is correctly calculated
this->realtimeData->use = 0;
this->realtimeData->produce = 0;
this->realtimeData->costHour = 0;
this->realtimeData->incomeHour = 0;
realtimeData->use = 0;
realtimeData->produce = 0;
realtimeData->costHour = 0;
realtimeData->incomeHour = 0;
uint8_t prevDay = this->realtimeData->currentDay;
if(local.Day != this->realtimeData->currentDay) {
data.costYesterday = this->realtimeData->costDay * 100;
data.costThisMonth += this->realtimeData->costDay * 100;
this->realtimeData->costDay = 0;
uint8_t prevDay = realtimeData->currentDay;
if(local.Day != realtimeData->currentDay) {
data.costYesterday = realtimeData->costDay * 100;
data.costThisMonth += realtimeData->costDay * 100;
realtimeData->costDay = 0;
data.incomeYesterday = this->realtimeData->incomeDay * 100;
data.incomeThisMonth += this->realtimeData->incomeDay * 100;
this->realtimeData->incomeDay = 0;
data.incomeYesterday = realtimeData->incomeDay * 100;
data.incomeThisMonth += realtimeData->incomeDay * 100;
realtimeData->incomeDay = 0;
this->realtimeData->currentDay = local.Day;
realtimeData->currentDay = local.Day;
ret = true;
}
@@ -145,41 +145,49 @@ bool EnergyAccounting::update(AmsData* amsData) {
data.lastMonthAccuracy = accuracy;
data.month = local.Month;
this->realtimeData->currentThresholdIdx = 0;
realtimeData->currentThresholdIdx = 0;
ret = true;
}
if(ret) {
data.costToday = realtimeData->costDay * 100;
data.incomeToday = realtimeData->incomeDay * 100;
data.lastUpdated = now;
}
}
if(this->realtimeData->lastImportUpdateMillis < amsData->getLastUpdateMillis()) {
unsigned long ms = amsData->getLastUpdateMillis() - this->realtimeData->lastImportUpdateMillis;
if(realtimeData->lastImportUpdateMillis < amsData->getLastUpdateMillis()) {
unsigned long ms = amsData->getLastUpdateMillis() - realtimeData->lastImportUpdateMillis;
float kwhi = (amsData->getActiveImportPower() * (((float) ms) / 3600000.0)) / 1000.0;
if(kwhi > 0) {
this->realtimeData->use += kwhi;
if(price != PRICE_NO_VALUE) {
float cost = price * kwhi;
this->realtimeData->costHour += cost;
this->realtimeData->costDay += cost;
realtimeData->use += kwhi;
float importPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_IMPORT);
if(importPrice != PRICE_NO_VALUE) {
float cost = importPrice * kwhi;
realtimeData->costHour += cost;
realtimeData->costDay += cost;
}
}
this->realtimeData->lastImportUpdateMillis = amsData->getLastUpdateMillis();
realtimeData->lastImportUpdateMillis = amsData->getLastUpdateMillis();
}
if(amsData->getListType() > 1 && this->realtimeData->lastExportUpdateMillis < amsData->getLastUpdateMillis()) {
unsigned long ms = amsData->getLastUpdateMillis() - this->realtimeData->lastExportUpdateMillis;
if(amsData->getListType() > 1 && realtimeData->lastExportUpdateMillis < amsData->getLastUpdateMillis()) {
unsigned long ms = amsData->getLastUpdateMillis() - realtimeData->lastExportUpdateMillis;
float kwhe = (amsData->getActiveExportPower() * (((float) ms) / 3600000.0)) / 1000.0;
if(kwhe > 0) {
this->realtimeData->produce += kwhe;
if(price != PRICE_NO_VALUE) {
float income = price * kwhe;
this->realtimeData->incomeHour += income;
this->realtimeData->incomeDay += income;
realtimeData->produce += kwhe;
float exportPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_EXPORT);
if(exportPrice != PRICE_NO_VALUE) {
float income = exportPrice * kwhe;
realtimeData->incomeHour += income;
realtimeData->incomeDay += income;
}
}
this->realtimeData->lastExportUpdateMillis = amsData->getLastUpdateMillis();
realtimeData->lastExportUpdateMillis = amsData->getLastUpdateMillis();
}
if(config != NULL) {
while(getMonthMax() > config->thresholds[this->realtimeData->currentThresholdIdx] && this->realtimeData->currentThresholdIdx < 10) this->realtimeData->currentThresholdIdx++;
while(getMonthMax() > config->thresholds[realtimeData->currentThresholdIdx] && realtimeData->currentThresholdIdx < 10) realtimeData->currentThresholdIdx++;
}
return ret;
@@ -187,28 +195,36 @@ bool EnergyAccounting::update(AmsData* amsData) {
void EnergyAccounting::calcDayCost() {
time_t now = time(nullptr);
tmElements_t local, utc;
tmElements_t local, utc, lastUpdateUtc;
if(tz == NULL) return;
breakTime(tz->toLocal(now), local);
if(ps == NULL) return;
if(getPriceForHour(PRICE_DIRECTION_IMPORT, 0) != PRICE_NO_VALUE) {
if(initPrice) {
this->realtimeData->costDay = 0;
this->realtimeData->incomeDay = 0;
if(ps->hasPrice()) {
breakTime(data.lastUpdated, lastUpdateUtc);
uint8_t calcFromHour = 0;
if(lastUpdateUtc.Day != local.Day || lastUpdateUtc.Month != local.Month || lastUpdateUtc.Year != local.Year) {
realtimeData->costDay = 0;
realtimeData->incomeDay = 0;
calcFromHour = 0;
} else {
realtimeData->costDay = data.costToday / 100.0;
realtimeData->incomeDay = data.incomeToday / 100.0;
calcFromHour = lastUpdateUtc.Hour;
}
for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) {
for(uint8_t i = calcFromHour; i < realtimeData->currentHour; i++) {
breakTime(now - ((local.Hour - i) * 3600), utc);
float priceIn = getPriceForHour(PRICE_DIRECTION_IMPORT, i - local.Hour);
float priceIn = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i - local.Hour);
if(priceIn != PRICE_NO_VALUE) {
int16_t wh = ds->getHourImport(utc.Hour);
this->realtimeData->costDay += priceIn * (wh / 1000.0);
realtimeData->costDay += priceIn * (wh / 1000.0);
}
float priceOut = getPriceForHour(PRICE_DIRECTION_EXPORT, i - local.Hour);
float priceOut = ps->getPriceForRelativeHour(PRICE_DIRECTION_EXPORT, i - local.Hour);
if(priceOut != PRICE_NO_VALUE) {
int16_t wh = ds->getHourExport(utc.Hour);
this->realtimeData->incomeDay += priceOut * (wh / 1000.0);
realtimeData->incomeDay += priceOut * (wh / 1000.0);
}
}
initPrice = true;
@@ -216,7 +232,7 @@ void EnergyAccounting::calcDayCost() {
}
float EnergyAccounting::getUseThisHour() {
return this->realtimeData->use;
return realtimeData->use;
}
float EnergyAccounting::getUseToday() {
@@ -226,7 +242,7 @@ float EnergyAccounting::getUseToday() {
if(now < FirmwareVersion::BuildEpoch) return 0.0;
tmElements_t utc, local;
breakTime(tz->toLocal(now), local);
for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) {
for(uint8_t i = 0; i < realtimeData->currentHour; i++) {
breakTime(now - ((local.Hour - i) * 3600), utc);
ret += ds->getHourImport(utc.Hour) / 1000.0;
}
@@ -237,18 +253,20 @@ float EnergyAccounting::getUseThisMonth() {
time_t now = time(nullptr);
if(now < FirmwareVersion::BuildEpoch) return 0.0;
float ret = 0;
for(uint8_t i = 1; i < this->realtimeData->currentDay; i++) {
for(uint8_t i = 1; i < realtimeData->currentDay; i++) {
ret += ds->getDayImport(i) / 1000.0;
}
return ret + getUseToday();
}
float EnergyAccounting::getUseLastMonth() {
return (data.lastMonthImport * pow(10, data.lastMonthAccuracy)) / 1000;
float ret = (data.lastMonthImport * pow(10, data.lastMonthAccuracy)) / 1000;
if(std::isnan(ret)) return 0.0;
return ret;
}
float EnergyAccounting::getProducedThisHour() {
return this->realtimeData->produce;
return realtimeData->produce;
}
float EnergyAccounting::getProducedToday() {
@@ -258,7 +276,7 @@ float EnergyAccounting::getProducedToday() {
if(now < FirmwareVersion::BuildEpoch) return 0.0;
tmElements_t utc, local;
breakTime(tz->toLocal(now), local);
for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) {
for(uint8_t i = 0; i < realtimeData->currentHour; i++) {
breakTime(now - ((local.Hour - i) * 3600), utc);
ret += ds->getHourExport(utc.Hour) / 1000.0;
}
@@ -269,22 +287,24 @@ float EnergyAccounting::getProducedThisMonth() {
time_t now = time(nullptr);
if(now < FirmwareVersion::BuildEpoch) return 0.0;
float ret = 0;
for(uint8_t i = 1; i < this->realtimeData->currentDay; i++) {
for(uint8_t i = 1; i < realtimeData->currentDay; i++) {
ret += ds->getDayExport(i) / 1000.0;
}
return ret + getProducedToday();
}
float EnergyAccounting::getProducedLastMonth() {
return (data.lastMonthExport * pow(10, data.lastMonthAccuracy)) / 1000;
float ret = (data.lastMonthExport * pow(10, data.lastMonthAccuracy)) / 1000;
if(std::isnan(ret)) return 0.0;
return ret;
}
float EnergyAccounting::getCostThisHour() {
return this->realtimeData->costHour;
return realtimeData->costHour;
}
float EnergyAccounting::getCostToday() {
return this->realtimeData->costDay;
return realtimeData->costDay;
}
float EnergyAccounting::getCostYesterday() {
@@ -300,11 +320,11 @@ float EnergyAccounting::getCostLastMonth() {
}
float EnergyAccounting::getIncomeThisHour() {
return this->realtimeData->incomeHour;
return realtimeData->incomeHour;
}
float EnergyAccounting::getIncomeToday() {
return this->realtimeData->incomeDay;
return realtimeData->incomeDay;
}
float EnergyAccounting::getIncomeYesterday() {
@@ -322,7 +342,7 @@ float EnergyAccounting::getIncomeLastMonth() {
uint8_t EnergyAccounting::getCurrentThreshold() {
if(config == NULL)
return 0;
return config->thresholds[this->realtimeData->currentThresholdIdx];
return config->thresholds[realtimeData->currentThresholdIdx];
}
float EnergyAccounting::getMonthMax() {
@@ -402,85 +422,31 @@ bool EnergyAccounting::load() {
char buf[file.size()];
file.readBytes(buf, file.size());
if(buf[0] == 6) {
if(buf[0] == 7) {
EnergyAccountingData* data = (EnergyAccountingData*) buf;
memcpy(&this->data, data, sizeof(this->data));
ret = true;
} else if(buf[0] == 5) {
EnergyAccountingData5* data = (EnergyAccountingData5*) buf;
this->data = { 6, data->month,
((uint32_t) data->costYesterday) * 10,
((uint32_t) data->costThisMonth) * 100,
((uint32_t) data->costLastMonth) * 100,
((uint32_t) data->incomeYesterday) * 10,
((uint32_t) data->incomeThisMonth) * 100,
((uint32_t) data->incomeLastMonth) * 100,
0,0,0, // Last month import, export and accuracy
data->peaks[0].day, data->peaks[0].value,
data->peaks[1].day, data->peaks[1].value,
data->peaks[2].day, data->peaks[2].value,
data->peaks[3].day, data->peaks[3].value,
data->peaks[4].day, data->peaks[4].value
};
ret = true;
} else if(buf[0] == 4) {
EnergyAccountingData4* data = (EnergyAccountingData4*) buf;
this->data = { 5, data->month,
((uint32_t) data->costYesterday) * 10,
((uint32_t) data->costThisMonth) * 100,
((uint32_t) data->costLastMonth) * 100,
0,0,0, // Income from production
0,0,0, // Last month import, export and accuracy
data->peaks[0].day, data->peaks[0].value,
data->peaks[1].day, data->peaks[1].value,
data->peaks[2].day, data->peaks[2].value,
data->peaks[3].day, data->peaks[3].value,
data->peaks[4].day, data->peaks[4].value
};
ret = true;
} else if(buf[0] == 3) {
EnergyAccountingData* data = (EnergyAccountingData*) buf;
this->data = { 5, data->month,
data->costYesterday * 10,
} else if(buf[0] == 6) {
EnergyAccountingData6* data = (EnergyAccountingData6*) buf;
this->data = { 7, data->month,
0, // Cost today
data->costYesterday,
data->costThisMonth,
data->costLastMonth,
0,0,0, // Income from production
0,0,0, // Last month import, export and accuracy
data->peaks[0].day, data->peaks[0].value,
data->peaks[1].day, data->peaks[1].value,
data->peaks[2].day, data->peaks[2].value,
data->peaks[3].day, data->peaks[3].value,
data->peaks[4].day, data->peaks[4].value
0, // Income today
data->incomeYesterday,
data->incomeThisMonth,
data->incomeLastMonth,
data->lastMonthImport,
data->lastMonthExport,
data->lastMonthAccuracy,
data->peaks[0].day, 0, data->peaks[0].value,
data->peaks[1].day, 0, data->peaks[1].value,
data->peaks[2].day, 0, data->peaks[2].value,
data->peaks[3].day, 0, data->peaks[3].value,
data->peaks[4].day, 0, data->peaks[4].value
};
ret = true;
} else {
data = { 5, 0,
0, 0, 0, // Cost
0,0,0, // Income from production
0,0,0, // Last month import, export and accuracy
0, 0, // Peak 1
0, 0, // Peak 2
0, 0, // Peak 3
0, 0, // Peak 4
0, 0 // Peak 5
};
if(buf[0] == 2) {
EnergyAccountingData2* data = (EnergyAccountingData2*) buf;
this->data.month = data->month;
this->data.costYesterday = data->costYesterday * 10;
this->data.costThisMonth = data->costThisMonth;
this->data.costLastMonth = data->costLastMonth;
uint8_t b = 0;
for(uint8_t i = sizeof(this->data); i < file.size(); i+=2) {
this->data.peaks[b].day = b;
memcpy(&this->data.peaks[b].value, buf+i, 2);
b++;
if(b >= config->hours || b >= 5) break;
}
ret = true;
} else {
ret = false;
}
}
file.close();
@@ -513,11 +479,12 @@ void EnergyAccounting::setData(EnergyAccountingData& data) {
this->data = data;
}
bool EnergyAccounting::updateMax(uint16_t val, uint8_t day) {
bool EnergyAccounting::updateMax(uint16_t val, uint8_t day, uint8_t hour) {
for(uint8_t i = 0; i < 5; i++) {
if(data.peaks[i].day == day || data.peaks[i].day == 0) {
if(val > data.peaks[i].value) {
data.peaks[i].day = day;
data.peaks[i].hour = hour;
data.peaks[i].value = val;
return true;
}
@@ -545,8 +512,3 @@ bool EnergyAccounting::updateMax(uint16_t val, uint8_t day) {
void EnergyAccounting::setCurrency(String currency) {
this->currency = currency;
}
float EnergyAccounting::getPriceForHour(uint8_t d, uint8_t h) {
if(ps == NULL) return PRICE_NO_VALUE;
return ps->getValueForHour(d, h);
}

View File

@@ -10,72 +10,35 @@
#include "AmsMqttHandler.h"
#include "HomeAssistantStatic.h"
#include "AmsConfiguration.h"
#include "hexutils.h"
class HomeAssistantMqttHandler : public AmsMqttHandler {
public:
HomeAssistantMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) {
#if defined(AMS_REMOTE_DEBUG)
HomeAssistantMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater, char* hostname) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
#else
HomeAssistantMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater, char* hostname) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
#endif
this->boardType = boardType;
this->hw = hw;
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = false;
topic = String(mqttConfig.publishTopic);
if(strlen(config.discoveryNameTag) > 0) {
snprintf_P(buf, 128, PSTR("AMS reader (%s)"), config.discoveryNameTag);
deviceName = String(buf);
snprintf_P(buf, 128, PSTR("[%s] "), config.discoveryNameTag);
sensorNamePrefix = String(buf);
} else {
deviceName = F("AMS reader");
sensorNamePrefix = "";
}
deviceModel = boardTypeToString(boardType);
manufacturer = boardManufacturerToString(boardType);
#if defined(ESP8266)
String hostname = WiFi.hostname();
#elif defined(ESP32)
String hostname = WiFi.getHostname();
#endif
deviceUid = hostname; // Maybe configurable in the future?
if(strlen(config.discoveryHostname) > 0) {
if(strncmp_P(config.discoveryHostname, PSTR("http"), 4) == 0) {
deviceUrl = String(config.discoveryHostname);
} else {
snprintf_P(buf, 128, PSTR("http://%s/"), config.discoveryHostname);
deviceUrl = String(buf);
}
} else {
snprintf_P(buf, 128, PSTR("http://%s.local/"), hostname);
deviceUrl = String(buf);
}
if(strlen(config.discoveryPrefix) > 0) {
snprintf_P(json, 128, PSTR("%s/status"), config.discoveryPrefix);
statusTopic = String(buf);
snprintf_P(buf, 128, PSTR("%s/sensor/"), config.discoveryPrefix);
discoveryTopic = String(buf);
} else {
statusTopic = F("homeassistant/status");
discoveryTopic = F("homeassistant/sensor/");
}
strcpy(this->mqttConfig.subscribeTopic, statusTopic.c_str());
setHomeAssistantConfig(config, hostname);
};
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*);
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
bool publishRaw(String data);
bool publishRaw(uint8_t* raw, size_t length);
bool publishFirmware();
bool postConnect();
void onMessage(String &topic, String &payload);
uint8_t getFormat();
void setHomeAssistantConfig(HomeAssistantConfig config, char* hostname);
private:
String topic;
uint8_t boardType;
String deviceName;
String deviceModel;
@@ -84,12 +47,14 @@ private:
String deviceUrl;
String statusTopic;
String discoveryTopic;
String sensorTopic;
String updateTopic;
String sensorNamePrefix;
bool l1Init, l2Init, l2eInit, l3Init, l3eInit, l4Init, l4eInit, rtInit, rteInit, pInit, sInit;
bool l1Init, l2Init, l2eInit, l3Init, l3eInit, l4Init, l4eInit, rtInit, rteInit, pInit, sInit, rInit, fInit, dInit;
bool tInit[32] = {false};
bool prInit[38] = {false};
uint8_t priceImportInit = 0, priceExportInit = 0;
uint32_t lastThresholdPublish = 0;
HwTools* hw;
@@ -113,6 +78,8 @@ private:
void publishTemperatureSensor(uint8_t index, String id);
void publishPriceSensors(PriceService* ps);
void publishSystemSensors();
void publishThresholdSensors();
void toJsonIsoTimestamp(time_t t, char* buf, size_t buflen);
String boardTypeToString(uint8_t b) {
switch(b) {

View File

@@ -17,110 +17,113 @@ struct HomeAssistantSensor {
const char* uom;
const char* devcl;
const char* stacl;
const char* uid;
};
const uint8_t List1SensorCount PROGMEM = 1;
const uint8_t List1SensorCount PROGMEM = 2;
const HomeAssistantSensor List1Sensors[List1SensorCount] PROGMEM = {
{"Active import", "/power", "P", 30, "W", "power", "measurement"}
{"Active import", "/power", "P", 30, "W", "power", "measurement", ""},
{"Data timestamp", "/power", "t", 30, "", "timestamp", "", ""}
};
const uint8_t List2SensorCount PROGMEM = 8;
const HomeAssistantSensor List2Sensors[List2SensorCount] PROGMEM = {
{"Reactive import", "/power", "Q", 30, "var", "reactive_power", "measurement"},
{"Reactive export", "/power", "QO", 30, "var", "reactive_power", "measurement"},
{"L1 current", "/power", "I1", 30, "A", "current", "measurement"},
{"L2 current", "/power", "I2", 30, "A", "current", "measurement"},
{"L3 current", "/power", "I3", 30, "A", "current", "measurement"},
{"L1 voltage", "/power", "U1", 30, "V", "voltage", "measurement"},
{"L2 voltage", "/power", "U2", 30, "V", "voltage", "measurement"},
{"L3 voltage", "/power", "U3", 30, "V", "voltage", "measurement"}
{"Reactive import", "/power", "Q", 30, "var", "reactive_power", "measurement", ""},
{"Reactive export", "/power", "QO", 30, "var", "reactive_power", "measurement", ""},
{"L1 current", "/power", "I1", 30, "A", "current", "measurement", ""},
{"L2 current", "/power", "I2", 30, "A", "current", "measurement", ""},
{"L3 current", "/power", "I3", 30, "A", "current", "measurement", ""},
{"L1 voltage", "/power", "U1", 30, "V", "voltage", "measurement", ""},
{"L2 voltage", "/power", "U2", 30, "V", "voltage", "measurement", ""},
{"L3 voltage", "/power", "U3", 30, "V", "voltage", "measurement", ""}
};
const uint8_t List2ExportSensorCount PROGMEM = 1;
const HomeAssistantSensor List2ExportSensors[List2ExportSensorCount] PROGMEM = {
{"Active export", "/power", "PO", 30, "W", "power", "measurement"}
{"Active export", "/power", "PO", 30, "W", "power", "measurement", ""}
};
const uint8_t List3SensorCount PROGMEM = 3;
const uint8_t List3SensorCount PROGMEM = 4;
const HomeAssistantSensor List3Sensors[List3SensorCount] PROGMEM = {
{"Accumulated active import", "/energy", "tPI", 4000, "kWh", "energy", "total_increasing"},
{"Accumulated reactive import","/energy", "tQI", 4000, "kvarh","", "total_increasing"},
{"Accumulated reactive export","/energy", "tQO", 4000, "kvarh","", "total_increasing"}
{"Accumulated active import", "/energy", "tPI", 4000, "kWh", "energy", "total_increasing", ""},
{"Accumulated reactive import","/energy", "tQI", 4000, "kvarh","", "total_increasing", ""},
{"Accumulated reactive export","/energy", "tQO", 4000, "kvarh","", "total_increasing", ""},
{"Meter timestamp", "/energy", "rtc", 4000, "", "timestamp", "", ""}
};
const uint8_t List3ExportSensorCount PROGMEM = 1;
const HomeAssistantSensor List3ExportSensors[List3ExportSensorCount] PROGMEM = {
{"Accumulated active export", "/energy", "tPO", 4000, "kWh", "energy", "total_increasing"}
{"Accumulated active export", "/energy", "tPO", 4000, "kWh", "energy", "total_increasing", ""}
};
const uint8_t List4SensorCount PROGMEM = 10;
const HomeAssistantSensor List4Sensors[List4SensorCount] PROGMEM = {
{"Power factor", "/power", "PF", 30, "%", "power_factor", "measurement"},
{"L1 power factor", "/power", "PF1", 30, "%", "power_factor", "measurement"},
{"L2 power factor", "/power", "PF2", 30, "%", "power_factor", "measurement"},
{"L3 power factor", "/power", "PF3", 30, "%", "power_factor", "measurement"},
{"L1 active import", "/power", "P1", 30, "W", "power", "measurement"},
{"L2 active import", "/power", "P2", 30, "W", "power", "measurement"},
{"L3 active import", "/power", "P3", 30, "W", "power", "measurement"},
{"L1 accumulated active import","/power", "tPI1", 30, "kWh", "energy", "total_increasing"},
{"L2 accumulated active import","/power", "tPI2", 30, "kWh", "energy", "total_increasing"},
{"L3 accumulated active import","/power", "tPI3", 30, "kWh", "energy", "total_increasing"}
{"Power factor", "/power", "PF", 30, "%", "power_factor", "measurement", ""},
{"L1 power factor", "/power", "PF1", 30, "%", "power_factor", "measurement", ""},
{"L2 power factor", "/power", "PF2", 30, "%", "power_factor", "measurement", ""},
{"L3 power factor", "/power", "PF3", 30, "%", "power_factor", "measurement", ""},
{"L1 active import", "/power", "P1", 30, "W", "power", "measurement", ""},
{"L2 active import", "/power", "P2", 30, "W", "power", "measurement", ""},
{"L3 active import", "/power", "P3", 30, "W", "power", "measurement", ""},
{"L1 accumulated active import","/power", "tPI1", 30, "kWh", "energy", "total_increasing", ""},
{"L2 accumulated active import","/power", "tPI2", 30, "kWh", "energy", "total_increasing", ""},
{"L3 accumulated active import","/power", "tPI3", 30, "kWh", "energy", "total_increasing", ""}
};
const uint8_t List4ExportSensorCount PROGMEM = 6;
const HomeAssistantSensor List4ExportSensors[List4ExportSensorCount] PROGMEM = {
{"L1 active export", "/power", "PO1", 30, "W", "power", "measurement"},
{"L2 active export", "/power", "PO2", 30, "W", "power", "measurement"},
{"L3 active export", "/power", "PO3", 30, "W", "power", "measurement"},
{"L1 accumulated active export","/power", "tPO1", 30, "kWh", "energy", "total_increasing"},
{"L2 accumulated active export","/power", "tPO2", 30, "kWh", "energy", "total_increasing"},
{"L3 accumulated active export","/power", "tPO3", 30, "kWh", "energy", "total_increasing"}
{"L1 active export", "/power", "PO1", 30, "W", "power", "measurement", ""},
{"L2 active export", "/power", "PO2", 30, "W", "power", "measurement", ""},
{"L3 active export", "/power", "PO3", 30, "W", "power", "measurement", ""},
{"L1 accumulated active export","/power", "tPO1", 30, "kWh", "energy", "total_increasing", ""},
{"L2 accumulated active export","/power", "tPO2", 30, "kWh", "energy", "total_increasing", ""},
{"L3 accumulated active export","/power", "tPO3", 30, "kWh", "energy", "total_increasing", ""}
};
const uint8_t RealtimeSensorCount PROGMEM = 8;
const HomeAssistantSensor RealtimeSensors[RealtimeSensorCount] PROGMEM = {
{"Month max", "/realtime","max", 120, "kWh", "energy", "total_increasing"},
{"Tariff threshold", "/realtime","threshold", 120, "kWh", "energy", "total_increasing"},
{"Current hour used", "/realtime","hour.use", 120, "kWh", "energy", "total_increasing"},
{"Current hour cost", "/realtime","hour.cost", 120, "", "monetary", ""},
{"Current day used", "/realtime","day.use", 120, "kWh", "energy", "total_increasing"},
{"Current day cost", "/realtime","day.cost", 120, "", "monetary", ""},
{"Current month used", "/realtime","month.use", 120, "kWh", "energy", "total_increasing"},
{"Current month cost", "/realtime","month.cost", 120, "", "monetary", ""}
{"Month max", "/realtime","max", 120, "kWh", "energy", "", ""},
{"Tariff threshold", "/realtime","threshold", 120, "kWh", "energy", "", ""},
{"Current hour used", "/realtime","hour.use", 120, "kWh", "energy", "total_increasing", ""},
{"Current hour cost", "/realtime","hour.cost", 120, "", "monetary", "", ""},
{"Current day used", "/realtime","day.use", 120, "kWh", "energy", "total_increasing", ""},
{"Current day cost", "/realtime","day.cost", 120, "", "monetary", "", ""},
{"Current month used", "/realtime","month.use", 120, "kWh", "energy", "total_increasing", ""},
{"Current month cost", "/realtime","month.cost", 120, "", "monetary", "", ""}
};
const uint8_t RealtimeExportSensorCount PROGMEM = 6;
const HomeAssistantSensor RealtimeExportSensors[RealtimeExportSensorCount] PROGMEM = {
{"Current hour produced", "/realtime","hour.produced", 120, "kWh", "energy", "total_increasing"},
{"Current hour income", "/realtime","hour.income", 120, "", "monetary", ""},
{"Current day produced", "/realtime","day.produced", 120, "kWh", "energy", "total_increasing"},
{"Current day income", "/realtime","day.income", 120, "", "monetary", ""},
{"Current month produced", "/realtime","month.produced", 120, "kWh", "energy", "total_increasing"},
{"Current month income", "/realtime","month.income", 120, "", "monetary", ""}
{"Current hour produced", "/realtime","hour.produced", 120, "kWh", "energy", "total_increasing", ""},
{"Current hour income", "/realtime","hour.income", 120, "", "monetary", "", ""},
{"Current day produced", "/realtime","day.produced", 120, "kWh", "energy", "total_increasing", ""},
{"Current day income", "/realtime","day.income", 120, "", "monetary", "", ""},
{"Current month produced", "/realtime","month.produced", 120, "kWh", "energy", "total_increasing", ""},
{"Current month income", "/realtime","month.income", 120, "", "monetary", "", ""}
};
const HomeAssistantSensor RealtimePeakSensor PROGMEM = {"Current month peak %d", "/realtime", "peaks[%d]", 4000, "kWh", "energy", ""};
const HomeAssistantSensor RealtimePeakSensor PROGMEM = {"Current month peak %d", "/realtime", "peaks[%d]", 4000, "kWh", "energy", "", ""};
const HomeAssistantSensor RealtimeThresholdSensor PROGMEM = {"Tariff threshold %d", "/realtime", "thresholds[%d]", 4000, "kWh", "energy", "", ""};
const uint8_t PriceSensorCount PROGMEM = 5;
const HomeAssistantSensor PriceSensors[PriceSensorCount] PROGMEM = {
{"Minimum price ahead", "/prices", "prices.min", 4000, "", "monetary", ""},
{"Maximum price ahead", "/prices", "prices.max", 4000, "", "monetary", ""},
{"Cheapest 1hr period ahead", "/prices", "prices.cheapest1hr",4000, "", "timestamp", ""},
{"Cheapest 3hr period ahead", "/prices", "prices.cheapest3hr",4000, "", "timestamp", ""},
{"Cheapest 6hr period ahead", "/prices", "prices.cheapest6hr",4000, "", "timestamp", ""}
{"Minimum price ahead", "/prices", "prices.min", 4000, "", "monetary", "", ""},
{"Maximum price ahead", "/prices", "prices.max", 4000, "", "monetary", "", ""},
{"Cheapest 1hr period ahead", "/prices", "prices.cheapest1hr", 4000, "", "timestamp", "", ""},
{"Cheapest 3hr period ahead", "/prices", "prices.cheapest3hr", 4000, "", "timestamp", "", ""},
{"Cheapest 6hr period ahead", "/prices", "prices.cheapest6hr", 4000, "", "timestamp", "", ""}
};
const HomeAssistantSensor PriceSensor PROGMEM = {"Price in %02d %s", "/prices", "prices['%d']", 4000, "", "monetary", ""};
const uint8_t SystemSensorCount PROGMEM = 3;
const HomeAssistantSensor SystemSensors[SystemSensorCount] PROGMEM = {
{"Status", "/state", "rssi", 180, "dBm", "signal_strength", "measurement"},
{"Supply volt", "/state", "vcc", 180, "V", "voltage", "measurement"},
{"Uptime", "/state", "up", 180, "s", "duration", "measurement"}
{"Status", "/state", "rssi", 180, "dBm", "signal_strength", "measurement", ""},
{"Supply volt", "/state", "vcc", 180, "V", "voltage", "measurement", ""},
{"Uptime", "/state", "up", 180, "s", "duration", "measurement", ""}
};
const HomeAssistantSensor TemperatureSensor PROGMEM = {"Temperature sensor %s", "/temperatures", "temperatures['%s']", 900, "°C", "temperature", "measurement"};
const HomeAssistantSensor TemperatureSensor PROGMEM = {"Temperature sensor %s", "/temperatures", "temperatures['%s']", 900, "°C", "temperature", "measurement", ""};
const HomeAssistantSensor DataSensor PROGMEM = {"Data", "/data", "data", 900, "", "", "", ""};
#endif

View File

@@ -1,3 +1,4 @@
{
"P" : %d
"P" : %lu,
"t" : %s
}

View File

@@ -3,5 +3,6 @@
"tPO" : %.3f,
"tQI" : %.3f,
"tQO" : %.3f,
"rtc" : %lu
"rtc" : %s,
"t" : %s
}

View File

@@ -2,14 +2,15 @@
"lv" : "%s",
"id" : "%s",
"type" : "%s",
"P" : %d,
"Q" : %d,
"PO" : %d,
"QO" : %d,
"P" : %lu,
"Q" : %lu,
"PO" : %lu,
"QO" : %lu,
"I1" : %.2f,
"I2" : %.2f,
"I3" : %.2f,
"U1" : %.2f,
"U2" : %.2f,
"U3" : %.2f
"U3" : %.2f,
"t" : %s
}

View File

@@ -2,30 +2,31 @@
"lv" : "%s",
"id" : "%s",
"type" : "%s",
"P" : %d,
"P1" : %d,
"P2" : %d,
"P3" : %d,
"Q" : %d,
"PO" : %d,
"PO1" : %d,
"PO2" : %d,
"PO3" : %d,
"QO" : %d,
"P" : %lu,
"P1" : %lu,
"P2" : %lu,
"P3" : %lu,
"Q" : %lu,
"PO" : %lu,
"PO1" : %lu,
"PO2" : %lu,
"PO3" : %lu,
"QO" : %lu,
"I1" : %.2f,
"I2" : %.2f,
"I3" : %.2f,
"U1" : %.2f,
"U2" : %.2f,
"U3" : %.2f,
"PF" : %.2f,
"PF1" : %.2f,
"PF2" : %.2f,
"PF3" : %.2f,
"PF" : %d,
"PF1" : %d,
"PF2" : %d,
"PF3" : %d,
"tPI1" : %.3f,
"tPI2" : %.3f,
"tPI3" : %.3f,
"tPO1" : %.3f,
"tPO2" : %.3f,
"tPO3" : %.3f
"tPO3" : %.3f,
"t" : %s
}

View File

@@ -3,7 +3,6 @@
"stat_t" : "%s%s",
"uniq_id" : "%s_%s",
"obj_id" : "%s_%s",
"unit_of_meas" : "%s",
"val_tpl" : "{{ value_json.%s | is_defined }}",
"expire_after" : %d,
"dev" : {
@@ -13,5 +12,8 @@
"sw" : "%s",
"mf" : "%s",
"cu" : "%s"
}%s%s%s%s%s%s
}
%s%s%s
%s%s%s
%s%s%s
}

View File

@@ -1,23 +0,0 @@
{
"max" : %.1f,
"peaks" : [ %s ],
"threshold" : %d,
"hour" : {
"use" : %.2f,
"cost" : %.2f,
"produced" : %.2f,
"income" : %.2f
},
"day" : {
"use" : %.2f,
"cost" : %.2f,
"produced" : %.2f,
"income" : %.2f
},
"month" : {
"use" : %.2f,
"cost" : %.2f,
"produced" : %.2f,
"income" : %.2f
}
}

View File

@@ -13,38 +13,116 @@
#include "json/ha3_json.h"
#include "json/ha4_json.h"
#include "json/hadiscover_json.h"
#include "json/realtime_json.h"
#include "FirmwareVersion.h"
#if defined(ESP32)
#include <esp_task_wdt.h>
#endif
bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
if(topic.isEmpty() || !mqtt.connected())
void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config, char* hostname) {
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = fInit = dInit = false;
if(strlen(config.discoveryNameTag) > 0) {
snprintf_P(json, 128, PSTR("AMS reader (%s)"), config.discoveryNameTag);
deviceName = String(json);
snprintf_P(json, 128, PSTR("[%s] "), config.discoveryNameTag);
sensorNamePrefix = String(json);
} else {
snprintf_P(json, 128, PSTR("AMS reader"));
deviceName = String(json);
sensorNamePrefix = "";
}
deviceModel = boardTypeToString(boardType);
manufacturer = boardManufacturerToString(boardType);
deviceUid = String(hostname); // Maybe configurable in the future?
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR(" Hostname is [%s]\n"), hostname);
if(strlen(config.discoveryHostname) > 0) {
if(strncmp_P(config.discoveryHostname, PSTR("http"), 4) == 0) {
deviceUrl = String(config.discoveryHostname);
} else {
snprintf_P(json, 128, PSTR("http://%s/"), config.discoveryHostname);
deviceUrl = String(json);
}
} else {
snprintf_P(json, 128, PSTR("http://%s.local/"), hostname);
deviceUrl = String(json);
}
if(strlen(config.discoveryPrefix) == 0) {
snprintf_P(config.discoveryPrefix, 64, PSTR("homeassistant"));
}
snprintf_P(json, 128, PSTR("%s/status"), config.discoveryPrefix);
statusTopic = String(json);
snprintf_P(json, 128, PSTR("%s/sensor"), config.discoveryPrefix);
sensorTopic = String(json);
snprintf_P(json, 128, PSTR("%s/update"), config.discoveryPrefix);
updateTopic = String(json);
strcpy(this->mqttConfig.subscribeTopic, statusTopic.c_str());
}
bool HomeAssistantMqttHandler::postConnect() {
bool ret = true;
if(!statusTopic.isEmpty()) {
if(mqtt.subscribe(statusTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Subscribed to [%s]\n"), statusTopic.c_str());
} else {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Unable to subscribe to [%s]\n"), statusTopic.c_str());
ret = false;
}
}
return ret;
}
bool HomeAssistantMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
if(pubTopic.isEmpty() || !connected())
return false;
if(time(nullptr) < FirmwareVersion::BuildEpoch)
return false;
if(data->getListType() >= 3) { // publish energy counts
publishList3(data, ea);
AmsData data;
if(mqttConfig.stateUpdate) {
uint64_t now = millis64();
if(now-lastStateUpdate < mqttConfig.stateUpdateInterval * 1000) return false;
data.apply(*previousState);
data.apply(*update);
lastStateUpdate = now;
} else {
data = *update;
}
if(data.getListType() >= 3 && !data.isCounterEstimated()) { // publish energy counts
publishList3(&data, ea);
mqtt.loop();
}
if(data->getListType() == 1) { // publish power counts
publishList1(data, ea);
if(data.getListType() == 1) { // publish power counts
publishList1(&data, ea);
mqtt.loop();
} else if(data->getListType() <= 3) { // publish power counts and volts/amps
publishList2(data, ea);
} else if(data.getListType() <= 3) { // publish power counts and volts/amps
publishList2(&data, ea);
mqtt.loop();
} else if(data->getListType() == 4) { // publish power counts and volts/amps/phase power and PF
publishList4(data, ea);
} else if(data.getListType() == 4) { // publish power counts and volts/amps/phase power and PF
publishList4(&data, ea);
mqtt.loop();
}
if(ea->isInitialized()) {
publishRealtime(data, ea, ps);
publishRealtime(&data, ea, ps);
mqtt.loop();
}
loop();
@@ -53,13 +131,21 @@ bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, En
bool HomeAssistantMqttHandler::publishList1(AmsData* data, EnergyAccounting* ea) {
publishList1Sensors();
snprintf_P(json, BufferSize, HA1_JSON, data->getActiveImportPower());
return mqtt.publish(topic + "/power", json);
char pt[24];
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
snprintf_P(json, BufferSize, HA1_JSON, data->getActiveImportPower(), pt);
return mqtt.publish(pubTopic + "/power", json);
}
bool HomeAssistantMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea) {
publishList2Sensors();
if(data->getActiveExportPower() > 0) publishList2ExportSensors();
char pt[24];
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
snprintf_P(json, BufferSize, HA3_JSON,
data->getListId().c_str(),
data->getMeterId().c_str(),
@@ -73,27 +159,41 @@ bool HomeAssistantMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea)
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage()
data->getL3Voltage(),
pt
);
return mqtt.publish(topic + "/power", json);
return mqtt.publish(pubTopic + "/power", json);
}
bool HomeAssistantMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea) {
publishList3Sensors();
if(data->getActiveExportCounter() > 0.0) publishList3ExportSensors();
char mt[24];
toJsonIsoTimestamp(data->getMeterTimestamp(), mt, sizeof(mt));
char pt[24];
memset(pt, 0, 24);
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
snprintf_P(json, BufferSize, HA2_JSON,
data->getActiveImportCounter(),
data->getActiveExportCounter(),
data->getReactiveImportCounter(),
data->getReactiveExportCounter(),
data->getMeterTimestamp()
mt,
pt
);
return mqtt.publish(topic + "/energy", json);
return mqtt.publish(pubTopic + "/energy", json);
}
bool HomeAssistantMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea) {
publishList4Sensors();
if(data->getL1ActiveExportPower() > 0 || data->getL2ActiveExportPower() > 0 || data->getL3ActiveExportPower() > 0) publishList4ExportSensors();
char pt[24];
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
snprintf_P(json, BufferSize, HA4_JSON,
data->getListId().c_str(),
data->getMeterId().c_str(),
@@ -114,18 +214,19 @@ bool HomeAssistantMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea)
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage(),
data->getPowerFactor() == 0 ? 1 : data->getPowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL1PowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL2PowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL3PowerFactor(),
data->getPowerFactor() == 0 ? 100 : (int) (data->getPowerFactor() * 100),
data->getPowerFactor() == 0 ? 100 : (int) (data->getL1PowerFactor() * 100),
data->getPowerFactor() == 0 ? 100 : (int) (data->getL2PowerFactor() * 100),
data->getPowerFactor() == 0 ? 100 : (int) (data->getL3PowerFactor() * 100),
data->getL1ActiveImportCounter(),
data->getL2ActiveImportCounter(),
data->getL3ActiveImportCounter(),
data->getL1ActiveExportCounter(),
data->getL2ActiveExportCounter(),
data->getL3ActiveExportCounter()
data->getL3ActiveExportCounter(),
pt
);
return mqtt.publish(topic + "/power", json);
return mqtt.publish(pubTopic + "/power", json);
}
String HomeAssistantMqttHandler::getMeterModel(AmsData* data) {
@@ -137,6 +238,7 @@ String HomeAssistantMqttHandler::getMeterModel(AmsData* data) {
bool HomeAssistantMqttHandler::publishRealtime(AmsData* data, EnergyAccounting* ea, PriceService* ps) {
publishRealtimeSensors(ea, ps);
if(ea->getProducedThisHour() > 0.0 || ea->getProducedToday() > 0.0 || ea->getProducedThisMonth() > 0.0) publishRealtimeExportSensors(ea, ps);
if(lastThresholdPublish == 0) publishThresholdSensors();
String peaks = "";
uint8_t peakCount = ea->getConfig()->hours;
if(peakCount > 5) peakCount = 5;
@@ -144,7 +246,7 @@ bool HomeAssistantMqttHandler::publishRealtime(AmsData* data, EnergyAccounting*
if(!peaks.isEmpty()) peaks += ",";
peaks += String(ea->getPeak(i).value / 100.0, 2);
}
snprintf_P(json, BufferSize, REALTIME_JSON,
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"max\":%.1f,\"peaks\":[%s],\"threshold\":%d,\"hour\":{\"use\":%.2f,\"cost\":%.2f,\"produced\":%.2f,\"income\":%.2f},\"day\":{\"use\":%.2f,\"cost\":%.2f,\"produced\":%.2f,\"income\":%.2f},\"month\":{\"use\":%.2f,\"cost\":%.2f,\"produced\":%.2f,\"income\":%.2f}"),
ea->getMonthMax(),
peaks.c_str(),
ea->getCurrentThreshold(),
@@ -161,25 +263,45 @@ bool HomeAssistantMqttHandler::publishRealtime(AmsData* data, EnergyAccounting*
ea->getProducedThisMonth(),
ea->getIncomeThisMonth()
);
return mqtt.publish(topic + "/realtime", json);
}
uint32_t ms = millis();
if(lastThresholdPublish == 0 || ms-lastThresholdPublish > 3600000) {
EnergyAccountingConfig* conf = ea->getConfig();
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"thresholds\": [%d,%d,%d,%d,%d,%d,%d,%d,%d]"),
conf->thresholds[0],
conf->thresholds[1],
conf->thresholds[2],
conf->thresholds[3],
conf->thresholds[4],
conf->thresholds[5],
conf->thresholds[6],
conf->thresholds[7],
conf->thresholds[8]
);
lastThresholdPublish = ms;
}
time_t now = time(nullptr);
char pt[24];
toJsonIsoTimestamp(now, pt, sizeof(pt));
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
json[pos++] = '}';
json[pos] = '\0';
return mqtt.publish(pubTopic + "/realtime", json);
}
bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
int count = hw->getTempSensorCount();
if(count < 2) return false;
int size = 32 + (count * 26);
char buf[size];
snprintf_P(buf, 24, PSTR("{\"temperatures\":{"));
uint16_t pos = snprintf_P(json, 24, PSTR("{\"temperatures\":{"));
for(int i = 0; i < count; i++) {
TempSensorData* data = hw->getTempSensorData(i);
if(data != NULL) {
char* pos = buf+strlen(buf);
String id = toHex(data->address, 8);
snprintf_P(pos, 26, PSTR("\"%s\":%.2f,"),
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%s\":%.2f,"),
id.c_str(),
data->lastRead
);
@@ -187,17 +309,23 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
publishTemperatureSensor(i+1, id);
}
}
char* pos = buf+strlen(buf);
snprintf_P(count == 0 ? pos : pos-1, 8, PSTR("}}"));
bool ret = mqtt.publish(topic + "/temperatures", buf);
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("}"));
time_t now = time(nullptr);
char pt[24];
toJsonIsoTimestamp(now, pt, sizeof(pt));
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("}"));
bool ret = mqtt.publish(pubTopic + "/temperatures", json);
loop();
return ret;
}
bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
if(topic.isEmpty() || !mqtt.connected())
if(pubTopic.isEmpty() || !connected())
return false;
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE)
if(!ps->hasPrice())
return false;
publishPriceSensors(ps);
@@ -210,7 +338,7 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
float values[38];
for(int i = 0;i < 38; i++) values[i] = PRICE_NO_VALUE;
for(uint8_t i = 0; i < 38; i++) {
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, now, i);
float val = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
values[i] = val;
if(val == PRICE_NO_VALUE) break;
@@ -258,83 +386,125 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
memset(ts1hr, 0, 24);
if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
sprintf_P(ts1hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts1hr, sizeof(ts1hr));
}
char ts3hr[24];
memset(ts3hr, 0, 24);
if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
sprintf_P(ts3hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts3hr, sizeof(ts3hr));
}
char ts6hr[24];
memset(ts6hr, 0, 24);
if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
sprintf_P(ts6hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts6hr, sizeof(ts6hr));
}
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{"), WiFi.macAddress().c_str());
for(uint8_t i = 0;i < 38; i++) {
if(values[i] == PRICE_NO_VALUE) {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%d\":null,"), i);
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{\"import\":["), WiFi.macAddress().c_str());
uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex();
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
float val = ps->getPricePoint(PRICE_DIRECTION_IMPORT, i);
if(val == PRICE_NO_VALUE) {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
} else {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%d\":%.4f,"), i, values[i]);
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
}
}
if(rteInit && ps->isExportPricesDifferentFromImport()) {
pos--;
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"export\":["));
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
float val = ps->getPricePoint(PRICE_DIRECTION_EXPORT, i);
if(val == PRICE_NO_VALUE) {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
} else {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
}
}
}
snprintf_P(json+pos, BufferSize-pos, PSTR("\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":\"%s\",\"cheapest3hr\":\"%s\",\"cheapest6hr\":\"%s\"}}"),
pos--;
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":%s,\"cheapest3hr\":%s,\"cheapest6hr\":%s}"),
min == INT16_MAX ? 0.0 : min,
max == INT16_MIN ? 0.0 : max,
ts1hr,
ts3hr,
ts6hr
);
char pt[24];
toJsonIsoTimestamp(now, pt, sizeof(pt));
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
bool ret = mqtt.publish(topic + "/prices", json, true, 0);
json[pos++] = '}';
json[pos] = '\0';
bool ret = mqtt.publish(pubTopic + "/prices", json, true, 0);
loop();
return ret;
}
bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) {
if(topic.isEmpty() || !mqtt.connected())
if(pubTopic.isEmpty() || !connected())
return false;
publishSystemSensors();
if(hw->getTemperature() > -50) publishTemperatureSensor(0, "");
snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\"}"),
time_t now = time(nullptr);
char pt[24];
toJsonIsoTimestamp(now, pt, sizeof(pt));
snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\",\"t\":%s}"),
WiFi.macAddress().c_str(),
mqttConfig.clientId,
(uint32_t) (millis64()/1000),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature(),
FirmwareVersion::VersionString
FirmwareVersion::VersionString,
pt
);
bool ret = mqtt.publish(topic + "/state", json);
bool ret = mqtt.publish(pubTopic + "/state", json);
loop();
return ret;
}
void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor sensor) {
String uid = String(sensor.path);
uid.replace(".", "");
uid.replace("[", "");
uid.replace("]", "");
uid.replace("'", "");
String uid;
if(strlen(sensor.uid) > 0) {
uid = String(sensor.uid);
} else {
uid = String(sensor.path);
uid.replace(".", "");
uid.replace("[", "");
uid.replace("]", "");
uid.replace("'", "");
}
snprintf_P(json, BufferSize, HADISCOVER_JSON,
sensorNamePrefix.c_str(),
sensor.name,
mqttConfig.publishTopic, sensor.topic,
deviceUid.c_str(), uid.c_str(),
deviceUid.c_str(), uid.c_str(),
sensor.uom,
sensor.path,
sensor.ttl,
deviceUid.c_str(),
@@ -343,14 +513,21 @@ void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor sensor) {
FirmwareVersion::VersionString,
manufacturer.c_str(),
deviceUrl.c_str(),
strlen_P(sensor.devcl) > 0 ? ",\"dev_cla\":\"" : "",
strlen_P(sensor.devcl) > 0 ? (char *) FPSTR(sensor.devcl) : "",
strlen_P(sensor.devcl) > 0 ? "\"" : "",
strlen_P(sensor.stacl) > 0 ? ",\"stat_cla\":\"" : "",
strlen_P(sensor.stacl) > 0 ? (char *) FPSTR(sensor.stacl) : "",
strlen_P(sensor.stacl) > 0 ? "\"" : ""
strlen_P(sensor.stacl) > 0 ? "\"" : "",
strlen_P(sensor.uom) > 0 ? ",\"unit_of_meas\":\"" : "",
strlen_P(sensor.uom) > 0 ? (char *) FPSTR(sensor.uom) : "",
strlen_P(sensor.uom) > 0 ? "\"" : ""
);
mqtt.publish(discoveryTopic + deviceUid + "_" + uid.c_str() + "/config", json, true, 0);
mqtt.publish(sensorTopic + "/" + deviceUid + "_" + uid + "/config", json, true, 0);
loop();
}
@@ -439,7 +616,8 @@ void HomeAssistantMqttHandler::publishRealtimeSensors(EnergyAccounting* ea, Pric
RealtimePeakSensor.ttl,
RealtimePeakSensor.uom,
RealtimePeakSensor.devcl,
RealtimePeakSensor.stacl
RealtimePeakSensor.stacl,
RealtimePeakSensor.uid
};
publishSensor(sensor);
}
@@ -478,7 +656,8 @@ void HomeAssistantMqttHandler::publishTemperatureSensor(uint8_t index, String id
TemperatureSensor.ttl,
TemperatureSensor.uom,
TemperatureSensor.devcl,
TemperatureSensor.stacl
TemperatureSensor.stacl,
TemperatureSensor.uid
};
publishSensor(sensor);
tInit[index] = true;
@@ -498,30 +677,96 @@ void HomeAssistantMqttHandler::publishPriceSensors(PriceService* ps) {
}
pInit = true;
}
for(uint8_t i = 0; i < 38; i++) {
if(prInit[i]) continue;
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, i);
if(val == PRICE_NO_VALUE) continue;
char name[strlen(PriceSensor.name)+2];
snprintf(name, strlen(PriceSensor.name)+2, PriceSensor.name, i, i == 1 ? "hour" : "hours");
char path[strlen(PriceSensor.path)+1];
snprintf(path, strlen(PriceSensor.path)+1, PriceSensor.path, i);
HomeAssistantSensor sensor = {
i == 0 ? "Price current hour" : name,
PriceSensor.topic,
path,
PriceSensor.ttl,
uom.c_str(),
PriceSensor.devcl,
i == 0 ? "total" : PriceSensor.stacl
};
publishSensor(sensor);
prInit[i] = true;
uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex();
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
if(priceImportInit < numberOfPoints-currentPricePointIndex) {
uint8_t importPriceSensorNo = 0;
for(int pricePointIndex = currentPricePointIndex; pricePointIndex < numberOfPoints; pricePointIndex++) {
float val = ps->getPricePoint(PRICE_DIRECTION_IMPORT, pricePointIndex);
if(val == PRICE_NO_VALUE) break;
if(importPriceSensorNo < priceImportInit) {
importPriceSensorNo++;
continue;
}
uint8_t resolution = ps->getResolutionInMinutes();
char path[64];
memset(path, 0, 64);
snprintf_P(path, 64, PSTR("prices.import[%d]"), importPriceSensorNo);
char uid[32];
memset(uid, 0, 32);
snprintf_P(uid, 32, PSTR("prices%d"), importPriceSensorNo);
char name[64];
if(resolution == 60)
snprintf_P(name, 64, PSTR("Import price in %02d hour%s"), importPriceSensorNo, importPriceSensorNo == 1 ? "" : "s");
else
snprintf_P(name, 64, PSTR("Import price in %03d minutes"), importPriceSensorNo * resolution);
HomeAssistantSensor sensor = {
importPriceSensorNo == 0 ? "Current import price" : name,
"/prices",
path,
resolution * 60 + 300,
uom.c_str(),
"monetary",
importPriceSensorNo == 0 ? "total" : "",
uid
};
publishSensor(sensor);
priceImportInit = importPriceSensorNo++;
}
}
if(priceExportInit < numberOfPoints-currentPricePointIndex) {
uint8_t exportPriceSensorNo = 0;
for(int pricePointIndex = currentPricePointIndex; pricePointIndex < numberOfPoints; pricePointIndex++) {
float val = ps->getPricePoint(PRICE_DIRECTION_EXPORT, pricePointIndex);
if(val == PRICE_NO_VALUE) break;
if(exportPriceSensorNo < priceExportInit) {
exportPriceSensorNo++;
continue;
}
uint8_t resolution = ps->getResolutionInMinutes();
char path[64];
memset(path, 0, 64);
snprintf_P(path, 64, PSTR("prices.export[%d]"), exportPriceSensorNo);
char uid[32];
memset(uid, 0, 32);
snprintf_P(uid, 32, PSTR("exportprices%d"), exportPriceSensorNo);
char name[64];
if(resolution == 60)
snprintf_P(name, 64, PSTR("Export price in %02d hour%s"), exportPriceSensorNo, exportPriceSensorNo == 1 ? "" : "s");
else
snprintf_P(name, 64, PSTR("Export price in %03d minutes"), exportPriceSensorNo * resolution);
HomeAssistantSensor sensor = {
exportPriceSensorNo == 0 ? "Current export price" : name,
"/prices",
path,
resolution * 60 + 300,
uom.c_str(),
"monetary",
exportPriceSensorNo == 0 ? "total" : "",
uid
};
publishSensor(sensor);
priceExportInit = exportPriceSensorNo++;
}
}
}
void HomeAssistantMqttHandler::publishSystemSensors() {
if(sInit) return;
for(uint8_t i = 0; i < SystemSensorCount; i++) {
@@ -530,21 +775,105 @@ void HomeAssistantMqttHandler::publishSystemSensors() {
sInit = true;
}
void HomeAssistantMqttHandler::publishThresholdSensors() {
if(rInit) return;
for(uint8_t i = 0; i < 9; i++) {
char name[strlen(RealtimeThresholdSensor.name)+1];
snprintf(name, strlen(RealtimeThresholdSensor.name)+2, RealtimeThresholdSensor.name, i+1);
char path[strlen(RealtimeThresholdSensor.path)+1];
snprintf(path, strlen(RealtimeThresholdSensor.path)+1, RealtimeThresholdSensor.path, i);
HomeAssistantSensor sensor = {
name,
RealtimeThresholdSensor.topic,
path,
RealtimeThresholdSensor.ttl,
RealtimeThresholdSensor.uom,
RealtimeThresholdSensor.devcl,
RealtimeThresholdSensor.stacl,
RealtimeThresholdSensor.uid
};
publishSensor(sensor);
}
rInit = true;
}
uint8_t HomeAssistantMqttHandler::getFormat() {
return 4;
}
bool HomeAssistantMqttHandler::publishRaw(String data) {
return false;
bool HomeAssistantMqttHandler::publishRaw(uint8_t* raw, size_t length) {
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
return false;
if(length <= 0 || length > BufferSize) return false;
if(!dInit) {
// Not sure how this sensor should be defined in HA, so skipping for now
//publishSensor(DataSensor);
dInit = true;
}
String str = toHex(raw, length);
snprintf_P(json, BufferSize, PSTR("{\"data\":\"%s\"}"), str.c_str());
char topic[192];
snprintf_P(topic, 192, PSTR("%s/data"), mqttConfig.publishTopic);
bool ret = mqtt.publish(topic, json);
loop();
return ret;
}
bool HomeAssistantMqttHandler::publishFirmware() {
if(!fInit) {
snprintf_P(json, BufferSize, PSTR("{\"name\":\"%sFirmware\",\"stat_t\":\"%s/firmware\",\"uniq_id\":\"%s_fwupgrade\",\"dev_cla\":\"firmware\",\"cmd_t\":\"%s\",\"pl_inst\":\"fwupgrade\"}"),
sensorNamePrefix.c_str(),
pubTopic.c_str(),
deviceUid.c_str(),
subTopic.c_str()
);
fInit = mqtt.publish(updateTopic + "/" + deviceUid + "/config", json, true, 0);
loop();
return fInit;
}
snprintf_P(json, BufferSize, PSTR("{\"installed_version\":\"%s\",\"latest_version\":\"%s\",\"title\":\"amsreader firmware\",\"release_url\":\"https://github.com/UtilitechAS/amsreader-firmware/releases\",\"release_summary\":\"New version %s is available\",\"update_percentage\":%s}"),
FirmwareVersion::VersionString,
strlen(updater->getNextVersion()) == 0 ? FirmwareVersion::VersionString : updater->getNextVersion(),
strlen(updater->getNextVersion()) == 0 ? FirmwareVersion::VersionString : updater->getNextVersion(),
updater->getProgress() < 0 ? "null" : String(updater->getProgress(), 0)
);
bool ret = mqtt.publish(pubTopic + "/firmware", json);
loop();
return ret;
}
void HomeAssistantMqttHandler::onMessage(String &topic, String &payload) {
if(topic.equals(statusTopic)) {
if(payload.equals("online")) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("Received online status from HA, resetting sensor status\n"));
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = false;
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Received online status from HA, resetting sensor status\n"));
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = fInit = dInit = false;
for(uint8_t i = 0; i < 32; i++) tInit[i] = false;
for(uint8_t i = 0; i < 38; i++) prInit[i] = false;
priceImportInit = 0;
priceExportInit = 0;
}
} else if(topic.equals(subTopic)) {
if(payload.equals("fwupgrade")) {
if(strcmp(updater->getNextVersion(), FirmwareVersion::VersionString) != 0) {
updater->setTargetVersion(updater->getNextVersion());
}
}
}
}
void HomeAssistantMqttHandler::toJsonIsoTimestamp(time_t t, char* buf, size_t buflen) {
memset(buf, 0, buflen);
if(t > 0) {
tmElements_t tm;
breakTime(t, tm);
snprintf_P(buf, buflen, PSTR("\"%04d-%02d-%02dT%02d:%02d:%02dZ\""), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
} else {
snprintf_P(buf, buflen, PSTR("null"));
}
}

View File

@@ -43,7 +43,7 @@ struct AdcConfig {
class HwTools {
public:
bool applyBoardConfig(uint8_t boardType, GpioConfig& gpioConfig, MeterConfig& meterConfig, uint8_t hanPin);
void setup(GpioConfig*);
void setup(SystemConfig* sys, GpioConfig* gpio);
float getVcc();
uint8_t getTempSensorCount();
TempSensorData* getTempSensorData(uint8_t);
@@ -56,15 +56,26 @@ public:
bool ledOff(uint8_t color);
bool ledBlink(uint8_t color, uint8_t blink);
void setBootSuccessful(bool value);
bool isVoltageOptimal(float range = 0.4);
uint8_t getBoardType();
HwTools() {};
private:
uint8_t boardType;
uint8_t ledPin, redPin, greenPin, bluePin, tempPin, atempPin;
uint8_t ledDisablePin, ledBehaviour;
bool ledInvert, rgbInvert;
uint8_t vccPin, vccGnd_r, vccVcc_r;
float vccOffset, vccMultiplier;
float vcc = 3.3; // Last known Vcc
float maxVcc = 3.25; // Best to have this close to max as a start, in case Pow-U reboots and starts off with a low voltage, we dont want that to be perceived as max
unsigned long lastVccRead = 0;
uint16_t analogRange = 1024;
AdcConfig voltAdc, tempAdc;
#if defined(ESP32)
esp_adc_cal_characteristics_t* voltAdcChar, tempAdcChar;
#endif
GpioConfig* config;
bool tempSensorInit;
OneWire *oneWire = NULL;
DallasTemperature *sensorApi = NULL;

View File

@@ -22,6 +22,9 @@ bool HwTools::applyBoardConfig(uint8_t boardType, GpioConfig& gpioConfig, MeterC
gpioConfig.vccResistorGnd = 22;
gpioConfig.vccResistorVcc = 33;
gpioConfig.ledDisablePin = 6;
gpioConfig.vccBootLimit = 0;
gpioConfig.vccOffset = 0;
gpioConfig.vccMultiplier = 0;
return true;
case 51: // Wemos S2 mini
gpioConfig.ledPin = 15;
@@ -75,6 +78,9 @@ bool HwTools::applyBoardConfig(uint8_t boardType, GpioConfig& gpioConfig, MeterC
case 243: // WT32-ETH01
meterConfig.rxPin = hanPin > 0 ? hanPin : 39;
return true;
case 245: // wESP32
gpioConfig.apPin = 0;
meterConfig.rxPin = hanPin > 0 ? hanPin : 39;
case 201: // D32
meterConfig.rxPin = hanPin > 0 ? hanPin : 16;
gpioConfig.apPin = 4;
@@ -144,8 +150,8 @@ bool HwTools::applyBoardConfig(uint8_t boardType, GpioConfig& gpioConfig, MeterC
return false;
}
void HwTools::setup(GpioConfig* config) {
this->config = config;
void HwTools::setup(SystemConfig* sys, GpioConfig* config) {
this->boardType = sys->boardType;
this->tempSensorInit = false;
if(sensorApi != NULL)
delete sensorApi;
@@ -153,8 +159,9 @@ void HwTools::setup(GpioConfig* config) {
delete oneWire;
if(config->tempSensorPin > 0 && config->tempSensorPin < 40) {
pinMode(config->tempSensorPin, INPUT);
tempPin = config->tempSensorPin;
} else {
config->tempSensorPin = 0xFF;
tempPin = config->tempSensorPin = 0xFF;
}
#if defined(CONFIG_IDF_TARGET_ESP32S2)
@@ -196,47 +203,63 @@ void HwTools::setup(GpioConfig* config) {
#else
pinMode(config->vccPin, INPUT);
#endif
vccPin = config->vccPin;
} else {
voltAdc.unit = 0xFF;
voltAdc.channel = 0xFF;
config->vccPin = 0xFF;
vccPin = config->vccPin = 0xFF;
}
vccOffset = config->vccOffset / 100.0;
vccMultiplier = config->vccMultiplier / 1000.0;
vccGnd_r = config->vccResistorGnd;
vccVcc_r = config->vccResistorVcc;
if(config->tempAnalogSensorPin > 0 && config->tempAnalogSensorPin < 40) {
pinMode(config->tempAnalogSensorPin, INPUT);
atempPin = config->tempAnalogSensorPin;
} else {
config->tempAnalogSensorPin = 0xFF;
atempPin = config->tempAnalogSensorPin = 0xFF;
}
if(config->ledPin > 0 && config->ledPin < 40) {
pinMode(config->ledPin, OUTPUT);
ledPin = config->ledPin;
ledInvert = config->ledInverted;
ledOff(LED_INTERNAL);
} else {
config->ledPin = 0xFF;
ledPin = config->ledPin = 0xFF;
}
if(config->ledPinRed > 0 && config->ledPinRed < 40) {
pinMode(config->ledPinRed, OUTPUT);
redPin = config->ledPinRed;
ledOff(LED_RED);
} else {
config->ledPinRed = 0xFF;
redPin = config->ledPinRed = 0xFF;
}
if(config->ledPinGreen > 0 && config->ledPinGreen < 40) {
pinMode(config->ledPinGreen, OUTPUT);
greenPin = config->ledPinGreen;
ledOff(LED_GREEN);
} else {
config->ledPinGreen = 0xFF;
greenPin = config->ledPinGreen = 0xFF;
}
if(config->ledPinBlue > 0 && config->ledPinBlue < 40) {
pinMode(config->ledPinBlue, OUTPUT);
bluePin = config->ledPinBlue;
ledOff(LED_BLUE);
} else {
config->ledPinBlue = 0xFF;
bluePin = config->ledPinBlue = 0xFF;
}
rgbInvert = config->ledRgbInverted;
if(config->ledDisablePin > 0 && config->ledDisablePin < 40) {
pinMode(config->ledDisablePin, OUTPUT_OPEN_DRAIN);
ledDisablePin = config->ledDisablePin;
ledBehaviour = config->ledBehaviour;
setBootSuccessful(false);
}
}
@@ -362,7 +385,7 @@ void HwTools::getAdcChannel(uint8_t pin, AdcConfig& config) {
float HwTools::getVcc() {
float volts = 0.0;
if(config->vccPin != 0xFF) {
if(vccPin != 0xFF) {
#if defined(ESP32)
if(voltAdc.unit != 0xFF) {
uint32_t x = 0;
@@ -385,32 +408,35 @@ float HwTools::getVcc() {
} else {
uint32_t x = 0;
for (int i = 0; i < 10; i++) {
x += analogRead(config->vccPin);
x += analogRead(vccPin);
}
volts = (x * 3.3) / 10.0 / analogRange;
}
#else
uint32_t x = 0;
for (int i = 0; i < 10; i++) {
x += analogRead(config->vccPin);
x += analogRead(vccPin);
}
volts = (x * 3.3) / 10.0 / analogRange;
#endif
} else {
}
if(volts == 0.0) {
#if defined(ESP8266)
volts = ESP.getVcc() / 1024.0;
#else
return 0.0;
#endif
}
if(volts == 0.0) return 0.0;
if(config->vccResistorGnd > 0 && config->vccResistorVcc > 0) {
volts *= ((float) (config->vccResistorGnd + config->vccResistorVcc) / config->vccResistorGnd);
}
float vccOffset = config->vccOffset / 100.0;
float vccMultiplier = config->vccMultiplier / 1000.0;
return vccOffset + (volts > 0.0 ? volts * vccMultiplier : 0.0);
if(vccGnd_r > 0 && vccVcc_r > 0)
volts *= ((float) (vccGnd_r + vccVcc_r) / vccGnd_r);
if(vccOffset != 0.0)
volts += vccOffset;
if(vccMultiplier != 0.0)
volts *= vccMultiplier;
return volts;
}
uint8_t HwTools::getTempSensorCount() {
@@ -425,9 +451,9 @@ TempSensorData* HwTools::getTempSensorData(uint8_t i) {
}
bool HwTools::updateTemperatures() {
if(config->tempSensorPin != 0xFF) {
if(tempPin != 0xFF) {
if(!tempSensorInit) {
oneWire = new OneWire(config->tempSensorPin);
oneWire = new OneWire(tempPin);
sensorApi = new DallasTemperature(this->oneWire);
sensorApi->begin();
delay(100);
@@ -513,9 +539,9 @@ float HwTools::getTemperature() {
return c == 0 ? DEVICE_DISCONNECTED_C : ret/c;
}
float HwTools::getTemperatureAnalog() {
if(config->tempAnalogSensorPin != 0xFF) {
if(atempPin != 0xFF) {
float adcCalibrationFactor = 1.06587;
int volts = ((float) analogRead(config->tempAnalogSensorPin) / analogRange) * 3.3;
int volts = ((float) analogRead(atempPin) / analogRange) * 3.3;
return ((volts * adcCalibrationFactor) - 0.4) / 0.0195;
}
return DEVICE_DISCONNECTED_C;
@@ -529,42 +555,42 @@ int HwTools::getWifiRssi() {
void HwTools::setBootSuccessful(bool value) {
if(bootSuccessful && value) return;
bootSuccessful = value;
if(config->ledDisablePin > 0 && config->ledDisablePin < 40) {
switch(config->ledBehaviour) {
if(ledDisablePin > 0 && ledDisablePin < 40) {
switch(ledBehaviour) {
case LED_BEHAVIOUR_ERROR_ONLY:
case LED_BEHAVIOUR_OFF:
digitalWrite(config->ledDisablePin, LOW);
digitalWrite(ledDisablePin, LOW);
break;
case LED_BEHAVIOUR_BOOT:
if(bootSuccessful) {
digitalWrite(config->ledDisablePin, LOW);
digitalWrite(ledDisablePin, LOW);
} else {
digitalWrite(config->ledDisablePin, HIGH);
digitalWrite(ledDisablePin, HIGH);
}
break;
default:
digitalWrite(config->ledDisablePin, HIGH);
digitalWrite(ledDisablePin, HIGH);
}
}
}
bool HwTools::ledOn(uint8_t color) {
if(config->ledBehaviour == LED_BEHAVIOUR_OFF) return false;
if(config->ledBehaviour == LED_BEHAVIOUR_ERROR_ONLY && color != LED_RED) return false;
if(config->ledBehaviour == LED_BEHAVIOUR_BOOT && color != LED_RED && bootSuccessful) return false;
if(ledBehaviour == LED_BEHAVIOUR_OFF) return false;
if(ledBehaviour == LED_BEHAVIOUR_ERROR_ONLY && color != LED_RED) return false;
if(ledBehaviour == LED_BEHAVIOUR_BOOT && color != LED_RED && bootSuccessful) return false;
if(color == LED_INTERNAL) {
return writeLedPin(color, config->ledInverted ? LOW : HIGH);
return writeLedPin(color, ledInvert ? LOW : HIGH);
} else {
return writeLedPin(color, config->ledRgbInverted ? LOW : HIGH);
return writeLedPin(color, rgbInvert ? LOW : HIGH);
}
}
bool HwTools::ledOff(uint8_t color) {
if(color == LED_INTERNAL) {
return writeLedPin(color, config->ledInverted ? HIGH : LOW);
return writeLedPin(color, ledInvert ? HIGH : LOW);
} else {
return writeLedPin(color, config->ledRgbInverted ? HIGH : LOW);
return writeLedPin(color, rgbInvert ? HIGH : LOW);
}
}
@@ -581,8 +607,8 @@ bool HwTools::ledBlink(uint8_t color, uint8_t blink) {
bool HwTools::writeLedPin(uint8_t color, uint8_t state) {
switch(color) {
case LED_INTERNAL: {
if(config->ledPin != 0xFF) {
digitalWrite(config->ledPin, state);
if(ledPin != 0xFF) {
digitalWrite(ledPin, state);
return true;
} else {
return false;
@@ -590,8 +616,8 @@ bool HwTools::writeLedPin(uint8_t color, uint8_t state) {
break;
}
case LED_RED: {
if(config->ledPinRed != 0xFF) {
digitalWrite(config->ledPinRed, state);
if(redPin != 0xFF) {
digitalWrite(redPin, state);
return true;
} else {
return false;
@@ -599,8 +625,8 @@ bool HwTools::writeLedPin(uint8_t color, uint8_t state) {
break;
}
case LED_GREEN: {
if(config->ledPinGreen != 0xFF) {
digitalWrite(config->ledPinGreen, state);
if(greenPin != 0xFF) {
digitalWrite(greenPin, state);
return true;
} else {
return false;
@@ -608,8 +634,8 @@ bool HwTools::writeLedPin(uint8_t color, uint8_t state) {
break;
}
case LED_BLUE: {
if(config->ledPinBlue != 0xFF) {
digitalWrite(config->ledPinBlue, state);
if(bluePin != 0xFF) {
digitalWrite(bluePin, state);
return true;
} else {
return false;
@@ -617,9 +643,9 @@ bool HwTools::writeLedPin(uint8_t color, uint8_t state) {
break;
}
case LED_YELLOW: {
if(config->ledPinRed != 0xFF && config->ledPinGreen != 0xFF) {
digitalWrite(config->ledPinRed, state);
digitalWrite(config->ledPinGreen, state);
if(redPin != 0xFF && greenPin != 0xFF) {
digitalWrite(redPin, state);
digitalWrite(greenPin, state);
return true;
} else {
return false;
@@ -629,3 +655,26 @@ bool HwTools::writeLedPin(uint8_t color, uint8_t state) {
}
return false;
}
bool HwTools::isVoltageOptimal(float range) {
if(boardType >= 1 && boardType <= 8 && maxVcc > 2.8) { // BUS-Power boards
unsigned long now = millis();
if(now - lastVccRead > 250) {
vcc = getVcc();
lastVccRead = now;
}
if(vcc > 3.4 || vcc < 2.8) {
maxVcc = 0; // Voltage is outside the operating range, we have to assume voltage is OK
} else if(vcc > maxVcc) {
maxVcc = vcc;
} else {
float diff = min(maxVcc, (float) 3.3) - vcc;
return diff < range;
}
}
return true;
}
uint8_t HwTools::getBoardType() {
return boardType;
}

View File

@@ -11,14 +11,20 @@
class JsonMqttHandler : public AmsMqttHandler {
public:
JsonMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) {
#if defined(AMS_REMOTE_DEBUG)
JsonMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, HwTools* hw, AmsDataStorage* ds, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
#else
JsonMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, HwTools* hw, AmsDataStorage* ds, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
#endif
this->hw = hw;
this->ds = ds;
};
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*);
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
bool publishRaw(String data);
bool publishRaw(uint8_t* raw, size_t length);
bool publishFirmware();
void onMessage(String &topic, String &payload);
@@ -26,6 +32,9 @@ public:
private:
HwTools* hw;
bool hasExport = false;
AmsDataStorage* ds;
uint16_t appendJsonHeader(AmsData* data);
uint16_t appendJsonFooter(EnergyAccounting* ea, uint16_t pos);
bool publishList1(AmsData* data, EnergyAccounting* ea);
@@ -33,5 +42,6 @@ private:
bool publishList3(AmsData* data, EnergyAccounting* ea);
bool publishList4(AmsData* data, EnergyAccounting* ea);
String getMeterModel(AmsData* data);
void toJsonIsoTimestamp(time_t t, char* buf, size_t buflen);
};
#endif

View File

@@ -8,34 +8,52 @@
#include "FirmwareVersion.h"
#include "hexutils.h"
#include "Uptime.h"
#include "AmsJsonGenerator.h"
bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
bool JsonMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
if(strlen(mqttConfig.publishTopic) == 0) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Unable to publish data, no publish topic\n"));
return false;
}
if(!mqtt.connected()) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Unable to publish data, not connected\n"));
if(!connected()) {
return false;
}
bool ret = false;
memset(json, 0, BufferSize);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Publishing list ID %d!\n"), data->getListType());
if(data->getListType() == 1) {
ret = publishList1(data, ea);
AmsData data;
if(mqttConfig.stateUpdate) {
uint64_t now = millis64();
if(now-lastStateUpdate < mqttConfig.stateUpdateInterval * 1000) return false;
data.apply(*previousState);
data.apply(*update);
lastStateUpdate = now;
} else {
data = *update;
}
if(data.getListType() == 1) {
ret = publishList1(&data, ea);
mqtt.loop();
} else if(data->getListType() == 2) {
ret = publishList2(data, ea);
} else if(data.getListType() == 2) {
ret = publishList2(&data, ea);
mqtt.loop();
} else if(data->getListType() == 3) {
ret = publishList3(data, ea);
} else if(data.getListType() == 3) {
ret = publishList3(&data, ea);
mqtt.loop();
} else if(data->getListType() == 4) {
ret = publishList4(data, ea);
} else if(data.getListType() == 4) {
ret = publishList4(&data, ea);
mqtt.loop();
}
if(data.getListType() >= 2 && data.getActiveExportPower() > 0.0) {
hasExport = true;
}
if(data.getListType() >= 3 && data.getActiveExportCounter() > 0.0) {
hasExport = true;
}
loop();
return ret;
}
@@ -59,14 +77,24 @@ uint16_t JsonMqttHandler::appendJsonFooter(EnergyAccounting* ea, uint16_t pos) {
} else {
memset(pf, 0, 4);
}
String peaks = "";
uint8_t peakCount = ea->getConfig()->hours;
if(peakCount > 5) peakCount = 5;
for(uint8_t i = 1; i <= peakCount; i++) {
if(!peaks.isEmpty()) peaks += ",";
peaks += String(ea->getPeak(i).value / 100.0, 2);
}
return snprintf_P(json+pos, BufferSize-pos, PSTR("%s\"%sh\":%.2f,\"%sd\":%.1f,\"%st\":%d,\"%sx\":%.2f,\"%she\":%.2f,\"%sde\":%.1f%s"),
return snprintf_P(json+pos, BufferSize-pos, PSTR("%s\"%sh\":%.3f,\"%sd\":%.2f,\"%sm\":%.1f,\"%st\":%d,\"%sx\":%.2f,\"%she\":%.3f,\"%sde\":%.2f,\"%sme\":%.1f,\"peaks\":[%s]%s"),
strlen(pf) == 0 ? "},\"realtime\":{" : ",",
pf,
ea->getUseThisHour(),
pf,
ea->getUseToday(),
pf,
ea->getUseThisMonth(),
pf,
ea->getCurrentThreshold(),
pf,
ea->getMonthMax(),
@@ -74,6 +102,9 @@ uint16_t JsonMqttHandler::appendJsonFooter(EnergyAccounting* ea, uint16_t pos) {
ea->getProducedThisHour(),
pf,
ea->getProducedToday(),
pf,
ea->getProducedThisMonth(),
peaks.c_str(),
strlen(pf) == 0 ? "}" : ""
);
}
@@ -85,7 +116,8 @@ bool JsonMqttHandler::publishList1(AmsData* data, EnergyAccounting* ea) {
}
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"P\":%d"), data->getActiveImportPower());
pos += appendJsonFooter(ea, pos);
json[pos] = '}';
json[pos++] = '}';
json[pos] = '\0';
if(mqttConfig.payloadFormat == 5) {
char topic[192];
snprintf_P(topic, 192, PSTR("%s/list1"), mqttConfig.publishTopic);
@@ -116,7 +148,8 @@ bool JsonMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea) {
data->getL3Voltage()
);
pos += appendJsonFooter(ea, pos);
json[pos] = '}';
json[pos++] = '}';
json[pos] = '\0';
if(mqttConfig.payloadFormat == 5) {
char topic[192];
snprintf_P(topic, 192, PSTR("%s/list2"), mqttConfig.publishTopic);
@@ -152,7 +185,8 @@ bool JsonMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea) {
data->getMeterTimestamp()
);
pos += appendJsonFooter(ea, pos);
json[pos] = '}';
json[pos++] = '}';
json[pos] = '\0';
if(mqttConfig.payloadFormat == 5) {
char topic[192];
snprintf_P(topic, 192, PSTR("%s/list3"), mqttConfig.publishTopic);
@@ -204,7 +238,8 @@ bool JsonMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea) {
data->getMeterTimestamp()
);
pos += appendJsonFooter(ea, pos);
json[pos] = '}';
json[pos++] = '}';
json[pos] = '\0';
if(mqttConfig.payloadFormat == 5) {
char topic[192];
snprintf_P(topic, 192, PSTR("%s/list4"), mqttConfig.publishTopic);
@@ -245,7 +280,8 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
bool ret = false;
json[pos-1] = '}';
if(mqttConfig.payloadFormat != 6) {
json[pos] = '}';
json[pos++] = '}';
json[pos] = '\0';
}
if(mqttConfig.payloadFormat == 5) {
char topic[192];
@@ -259,9 +295,9 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
}
bool JsonMqttHandler::publishPrices(PriceService* ps) {
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
if(strlen(mqttConfig.publishTopic) == 0 || !connected())
return false;
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE)
if(!ps->hasPrice())
return false;
time_t now = time(nullptr);
@@ -272,7 +308,7 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
float values[38];
for(int i = 0;i < 38; i++) values[i] = PRICE_NO_VALUE;
for(uint8_t i = 0; i < 38; i++) {
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, now, i);
float val = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
values[i] = val;
if(val == PRICE_NO_VALUE) break;
@@ -320,58 +356,89 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
memset(ts1hr, 0, 24);
if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
sprintf_P(ts1hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts1hr, sizeof(ts1hr));
}
char ts3hr[24];
memset(ts3hr, 0, 24);
if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
sprintf_P(ts3hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts3hr, sizeof(ts3hr));
}
char ts6hr[24];
memset(ts6hr, 0, 24);
if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
sprintf_P(ts6hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts6hr, sizeof(ts6hr));
}
char pf[4];
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\","), WiFi.macAddress().c_str());
if(mqttConfig.payloadFormat != 6) {
memset(pf, 0, 4);
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"prices\":{"));
} else {
strcpy_P(pf, PSTR("pr_"));
}
if(mqttConfig.payloadFormat == 6) {
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\","), WiFi.macAddress().c_str());
for(uint8_t i = 0;i < 38; i++) {
if(values[i] == PRICE_NO_VALUE) {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%s%d\":null,"), pf, i);
} else {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%s%d\":%.4f,"), pf, i, values[i]);
for(uint8_t i = 0;i < 38; i++) {
if(values[i] == PRICE_NO_VALUE) {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_%d\":null,"), i);
} else {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_%d\":%.4f,"), i, values[i]);
}
}
}
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%smin\":%.4f,\"%smax\":%.4f,\"%scheapest1hr\":\"%s\",\"%scheapest3hr\":\"%s\",\"%scheapest6hr\":\"%s\"}"),
pf,
min == INT16_MAX ? 0.0 : min,
pf,
max == INT16_MIN ? 0.0 : max,
pf,
ts1hr,
pf,
ts3hr,
pf,
ts6hr
);
if(mqttConfig.payloadFormat != 6) {
json[pos] = '}';
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_min\":%.4f,\"pr_max\":%.4f,\"pr_cheapest1hr\":%s,\"pr_cheapest3hr\":%s,\"pr_cheapest6hr\":%s}"),
min == INT16_MAX ? 0.0 : min,
max == INT16_MIN ? 0.0 : max,
ts1hr,
ts3hr,
ts6hr
);
} else {
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{\"import\":["), WiFi.macAddress().c_str());
uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex();
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
float val = ps->getPricePoint(PRICE_DIRECTION_IMPORT, i);
if(val == PRICE_NO_VALUE) {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
} else {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
}
}
if(hasExport && ps->isExportPricesDifferentFromImport()) {
pos--;
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"export\":["));
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
float val = ps->getPricePoint(PRICE_DIRECTION_EXPORT, i);
if(val == PRICE_NO_VALUE) {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
} else {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
}
}
}
pos--;
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":%s,\"cheapest3hr\":%s,\"cheapest6hr\":%s}}"),
min == INT16_MAX ? 0.0 : min,
max == INT16_MIN ? 0.0 : max,
ts1hr,
ts3hr,
ts6hr
);
}
bool ret = false;
if(mqttConfig.payloadFormat == 5) {
@@ -386,7 +453,7 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
}
bool JsonMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) {
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
if(strlen(mqttConfig.publishTopic) == 0 || !connected())
return false;
snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\"}"),
@@ -414,9 +481,77 @@ uint8_t JsonMqttHandler::getFormat() {
return 0;
}
bool JsonMqttHandler::publishRaw(String data) {
return false;
bool JsonMqttHandler::publishRaw(uint8_t* raw, size_t length) {
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
return false;
if(length <= 0 || length > BufferSize) return false;
String str = toHex(raw, length);
snprintf_P(json, BufferSize, PSTR("{\"data\":\"%s\"}"), str.c_str());
char topic[192];
snprintf_P(topic, 192, PSTR("%s/data"), mqttConfig.publishTopic);
bool ret = mqtt.publish(topic, json);
loop();
return ret;
}
bool JsonMqttHandler::publishFirmware() {
snprintf_P(json, BufferSize, PSTR("{\"installed_version\":\"%s\",\"latest_version\":\"%s\",\"title\":\"amsreader firmware\",\"release_url\":\"https://github.com/UtilitechAS/amsreader-firmware/releases\",\"release_summary\":\"New version %s is available\",\"update_percentage\":%s}"),
FirmwareVersion::VersionString,
strlen(updater->getNextVersion()) == 0 ? FirmwareVersion::VersionString : updater->getNextVersion(),
strlen(updater->getNextVersion()) == 0 ? FirmwareVersion::VersionString : updater->getNextVersion(),
updater->getProgress() < 0 ? "null" : String(updater->getProgress(), 0)
);
char topic[192];
snprintf_P(topic, 192, PSTR("%s/firmware"), mqttConfig.publishTopic);
bool ret = mqtt.publish(topic, json);
loop();
return ret;
}
void JsonMqttHandler::onMessage(String &topic, String &payload) {
if(strlen(mqttConfig.publishTopic) == 0 || !connected())
return;
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Received command [%s] to [%s]\n"), payload.c_str(), topic.c_str());
if(topic.equals(subTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR(" - this is our subscribed topic\n"));
if(payload.equals("fwupgrade")) {
if(strcmp(updater->getNextVersion(), FirmwareVersion::VersionString) != 0) {
updater->setTargetVersion(updater->getNextVersion());
}
} else if(payload.equals("dayplot")) {
char pubTopic[192];
snprintf_P(pubTopic, 192, PSTR("%s/dayplot"), mqttConfig.publishTopic);
AmsJsonGenerator::generateDayPlotJson(ds, json, BufferSize);
bool ret = mqtt.publish(pubTopic, json);
loop();
} else if(payload.equals("monthplot")) {
char pubTopic[192];
snprintf_P(pubTopic, 192, PSTR("%s/monthplot"), mqttConfig.publishTopic);
AmsJsonGenerator::generateMonthPlotJson(ds, json, BufferSize);
bool ret = mqtt.publish(pubTopic, json);
loop();
}
}
}
void JsonMqttHandler::toJsonIsoTimestamp(time_t t, char* buf, size_t buflen) {
memset(buf, 0, buflen);
if(t > 0) {
tmElements_t tm;
breakTime(t, tm);
snprintf_P(buf, buflen, PSTR("\"%04d-%02d-%02dT%02d:%02d:%02dZ\""), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
} else {
snprintf_P(buf, buflen, PSTR("null"));
}
}

View File

@@ -11,6 +11,9 @@
#include "AmsConfiguration.h"
#include "DataParser.h"
#include "Cosem.h"
#if defined(AMS_REMOTE_DEBUG)
#include "RemoteDebug.h"
#endif
#define NOVALUE 0xFFFFFFFF
@@ -21,7 +24,11 @@ struct AmsOctetTimestamp {
class IEC6205675 : public AmsData {
public:
IEC6205675(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state);
#if defined(AMS_REMOTE_DEBUG)
IEC6205675(const char* payload, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, RemoteDebug* debugger);
#else
IEC6205675(const char* payload, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, Stream* debugger);
#endif
private:
CosemData* getCosemDataAt(uint8_t index, const char* ptr);
@@ -30,6 +37,9 @@ private:
float getNumber(uint8_t* obis, int matchlength, const char* ptr);
float getNumber(CosemData*);
time_t getTimestamp(uint8_t* obis, int matchlength, const char* ptr);
time_t adjustForKnownIssues(CosemDateTime dt, Timezone* tz, uint8_t meterType);
uint8_t AMS_OBIS_UNKNOWN_1[4] = { 25, 9, 0, 255 };
uint8_t AMS_OBIS_VERSION[4] = { 0, 2, 129, 255 };
uint8_t AMS_OBIS_METER_MODEL[4] = { 96, 1, 1, 255 };

View File

@@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#ifndef _IMPULSEAMSDATA_H
#define _IMPULSEAMSDATA_H

View File

@@ -0,0 +1,39 @@
/**
* @copyright Utilitech AS 2024
* License: Fair Source
*
*/
#pragma once
#include "PassiveMeterCommunicator.h"
#if defined(AMS_REMOTE_DEBUG)
#include "RemoteDebug.h"
#endif
#include "AmsConfiguration.h"
#include "Timezone.h"
#include "ImpulseAmsData.h"
#if defined(ESP8266)
#include "SoftwareSerial.h"
#endif
#include "KmpTalker.h"
class KmpCommunicator : public PassiveMeterCommunicator {
public:
#if defined(AMS_REMOTE_DEBUG)
KmpCommunicator(RemoteDebug* debugger) : PassiveMeterCommunicator(debugger) {};
#else
KmpCommunicator(Stream* debugger) : PassiveMeterCommunicator(debugger) {};
#endif
void configure(MeterConfig&);
bool loop();
AmsData* getData(AmsData& meterState);
int getLastError();
void getCurrentConfig(MeterConfig& meterConfig) {
meterConfig = this->meterConfig;
}
private:
KmpTalker* talker = NULL;
};

View File

@@ -0,0 +1,56 @@
/**
* @copyright Utilitech AS 2023
* License: All rights reserved
*
*/
#pragma once
#include <Stream.h>
#define DATA_PARSE_OK 0
#define DATA_PARSE_FAIL -1
#define DATA_PARSE_INCOMPLETE -2
#define DATA_PARSE_FOOTER_CHECKSUM_ERROR -5
struct KmpParserContext {
uint8_t type;
uint16_t length;
};
struct KmpDataHolder {
uint32_t activeImportPower = 0, reactiveImportPower = 0, activeExportPower = 0, reactiveExportPower = 0;
float l1voltage = 0, l2voltage = 0, l3voltage = 0, l1current = 0, l2current = 0, l3current = 0;
uint32_t l1activeImportPower = 0, l2activeImportPower = 0, l3activeImportPower = 0;
uint32_t l1activeExportPower = 0, l2activeExportPower = 0, l3activeExportPower = 0;
double l1activeImportCounter = 0, l2activeImportCounter = 0, l3activeImportCounter = 0;
double l1activeExportCounter = 0, l2activeExportCounter = 0, l3activeExportCounter = 0;
float powerFactor = 0, l1PowerFactor = 0, l2PowerFactor = 0, l3PowerFactor = 0;
double activeImportCounter = 0, reactiveImportCounter = 0, activeExportCounter = 0, reactiveExportCounter = 0;
uint16_t meterId;
};
class KmpTalker {
public:
KmpTalker(Stream *hanSerial, uint8_t* hanBuffer, uint16_t hanBufferSize);
bool loop();
void getData(KmpDataHolder& data);
int getLastError();
private:
Stream *hanSerial;
uint8_t *hanBuffer = NULL;
uint16_t hanBufferSize = 0;
bool dataAvailable = false;
int len = 0;
int pos = DATA_PARSE_INCOMPLETE;
int lastError = DATA_PARSE_OK;
bool serialInit = false;
uint64_t lastUpdate = 0;
uint8_t batch = 0;
KmpParserContext ctx;
KmpDataHolder state;
};

View File

@@ -11,7 +11,6 @@
#include "AmsConfiguration.h"
#include "DataParser.h"
#include "Cosem.h"
#include "RemoteDebug.h"
struct LngHeader {
uint8_t tag;
@@ -31,7 +30,7 @@ struct LngObisDescriptor {
class LNG : public AmsData {
public:
LNG(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, RemoteDebug* debugger);
LNG(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx);
uint64_t getNumber(CosemData* item);
};

View File

@@ -11,9 +11,23 @@
#include "AmsConfiguration.h"
#include "DataParser.h"
#include "Cosem.h"
#include "RemoteDebug.h"
struct Lng2Data_3p {
struct Lng2Data_3p_0b {
CosemBasic header;
CosemLongUnsigned u1;
CosemLongUnsigned u2;
CosemLongUnsigned u3;
CosemLongUnsigned i1;
CosemLongUnsigned i2;
CosemLongUnsigned i3;
CosemDLongUnsigned activeImport;
CosemDLongUnsigned activeExport;
CosemDLongUnsigned acumulatedImport;
CosemDLongUnsigned accumulatedExport;
CosemString meterId;
} __attribute__((packed));
struct Lng2Data_3p_0e {
CosemBasic header;
CosemLongUnsigned u1;
CosemLongUnsigned u2;
@@ -33,7 +47,7 @@ struct Lng2Data_3p {
class LNG2 : public AmsData {
public:
LNG2(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, RemoteDebug* debugger);
LNG2(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx);
private:
uint8_t getString(CosemData* item, char* target);

View File

@@ -8,9 +8,12 @@
#define _METERCOMMUNICATOR_H
#include <Arduino.h>
#if defined(AMS_REMOTE_DEBUG)
#include "RemoteDebug.h"
#endif
#include "AmsData.h"
#include "AmsConfiguration.h"
#include "AmsMqttHandler.h"
class MeterCommunicator {
public:
@@ -20,7 +23,15 @@ public:
virtual AmsData* getData(AmsData& meterState);
virtual int getLastError();
virtual bool isConfigChanged();
virtual void ackConfigChanged();
virtual void getCurrentConfig(MeterConfig& meterConfig);
virtual void setMqttHandlerForDebugging(AmsMqttHandler* mqttHandler) {
this->mqttDebug = mqttHandler;
};
protected:
AmsMqttHandler* mqttDebug = NULL;
};
#endif

View File

@@ -8,38 +8,51 @@
#define _PASSIVEMETERCOMMUNICATOR_H
#include "MeterCommunicator.h"
#if defined(AMS_REMOTE_DEBUG)
#include "RemoteDebug.h"
#endif
#include "AmsConfiguration.h"
#include "DataParsers.h"
#include "Timezone.h"
#include "PassthroughMqttHandler.h"
#include "AmsMqttHandler.h"
#if defined(ESP8266)
#include "SoftwareSerial.h"
#endif
const uint32_t AUTO_BAUD_RATES[] = { 2400, 9600, 115200 };
class PassiveMeterCommunicator : public MeterCommunicator {
public:
#if defined(AMS_REMOTE_DEBUG)
PassiveMeterCommunicator(RemoteDebug* debugger);
#else
PassiveMeterCommunicator(Stream* debugger);
#endif
void configure(MeterConfig&, Timezone*);
bool loop();
AmsData* getData(AmsData& meterState);
int getLastError();
bool isConfigChanged();
void ackConfigChanged();
void getCurrentConfig(MeterConfig& meterConfig);
void setPassthroughMqttHandler(PassthroughMqttHandler*);
void setTimezone(Timezone* tz) {
this->tz = tz;
};
HardwareSerial* getHwSerial();
void rxerr(int err);
protected:
#if defined(AMS_REMOTE_DEBUG)
RemoteDebug* debugger = NULL;
#else
Stream* debugger = NULL;
#endif
MeterConfig meterConfig;
bool configChanged = false;
Timezone* tz;
PassthroughMqttHandler* pt = NULL;
uint8_t *hanBuffer = NULL;
uint16_t hanBufferSize = 0;
Stream *hanSerial;
@@ -49,12 +62,14 @@ protected:
HardwareSerial *hwSerial = NULL;
uint8_t rxBufferErrors = 0;
bool autodetect = false, validDataReceived = false;
bool autodetect = false;
uint8_t validDataReceived = 0;
unsigned long meterAutodetectLastChange = 0;
uint8_t meterAutoIndex = 0;
uint32_t bauds[6];
uint8_t parities[6];
bool inverts[6];
long rate = 10000;
uint32_t autodetectBaud = 0;
uint8_t autodetectParity = 11; // 8E1
bool autodetectInvert = false;
uint8_t autodetectCount = 0;
bool dataAvailable = false;
int len = 0;
@@ -73,11 +88,11 @@ protected:
DLMSParser *dlmsParser = NULL;
DSMRParser *dsmrParser = NULL;
void setupHanPort(uint32_t baud, uint8_t parityOrdinal, bool invert);
void setupHanPort(uint32_t baud, uint8_t parityOrdinal, bool invert, bool passive = true);
int16_t unwrapData(uint8_t *buf, DataParserContext &context);
void debugPrint(byte *buffer, int start, int length);
void printHanReadError(int pos);
void handleAutodetect(unsigned long now);
uint8_t getNextParity(uint8_t parityOrdinal);
};
#endif

View File

@@ -8,25 +8,36 @@
#define _PULSEMETERCOMMUNICATOR_H
#include "MeterCommunicator.h"
#if defined(AMS_REMOTE_DEBUG)
#include "RemoteDebug.h"
#endif
#include "AmsConfiguration.h"
#include "Timezone.h"
#include "ImpulseAmsData.h"
class PulseMeterCommunicator : public MeterCommunicator {
public:
#if defined(AMS_REMOTE_DEBUG)
PulseMeterCommunicator(RemoteDebug* debugger);
#else
PulseMeterCommunicator(Stream* debugger);
#endif
void configure(MeterConfig& config, Timezone* tz);
bool loop();
AmsData* getData(AmsData& meterState);
int getLastError();
bool isConfigChanged();
void ackConfigChanged();
void getCurrentConfig(MeterConfig& meterConfig);
void onPulse(uint8_t pulses);
protected:
#if defined(AMS_REMOTE_DEBUG)
RemoteDebug* debugger = NULL;
#else
Stream* debugger = NULL;
#endif
MeterConfig meterConfig;
bool configChanged = false;
Timezone* tz;

View File

@@ -11,6 +11,8 @@ IEC6205621::IEC6205621(const char* p, Timezone* tz, MeterConfig* meterConfig) {
if(strlen(p) < 16)
return;
this->packageTimestamp = time(nullptr);
String payload(p+1);
lastUpdateMillis = millis64();
@@ -59,15 +61,44 @@ IEC6205621::IEC6205621(const char* p, Timezone* tz, MeterConfig* meterConfig) {
}
}
tmElements_t tm { 0, 0, 0, 0, 0, 0, 0 };
String timestamp = extract(payload, F("1.0.0"));
if(timestamp.length() > 10) {
tmElements_t tm;
tm.Year = (timestamp.substring(0,2).toInt() + 2000) - 1970;
tm.Month = timestamp.substring(4,6).toInt();
tm.Day = timestamp.substring(2,4).toInt();
tm.Hour = timestamp.substring(6,8).toInt();
tm.Minute = timestamp.substring(8,10).toInt();
tm.Second = timestamp.substring(10,12).toInt();
if(timestamp.length() == 13) { // yyMMddHHmmssX
char x = timestamp.charAt(12);
if(x == 'S' || x == 'W') {
tm.Year = (timestamp.substring(0,2).toInt() + 2000) - 1970;
tm.Month = timestamp.substring(2,4).toInt();
tm.Day = timestamp.substring(4,6).toInt();
tm.Hour = timestamp.substring(6,8).toInt();
tm.Minute = timestamp.substring(8,10).toInt();
tm.Second = timestamp.substring(10,12).toInt();
}
} else if(timestamp.length() == 17) { // yyyyMMdd HH:mm:ss
char x = timestamp.charAt(11);
char y = timestamp.charAt(14);
if(x == ':' && y == ':') {
tm.Year = (timestamp.substring(0,4).toInt()) - 1970;
tm.Month = timestamp.substring(4,6).toInt();
tm.Day = timestamp.substring(6,8).toInt();
tm.Hour = timestamp.substring(9,11).toInt();
tm.Minute = timestamp.substring(12,14).toInt();
tm.Second = timestamp.substring(15,17).toInt();
}
} else if(timestamp.length() == 19) { // yyyy-MM-dd HH:mm:ss
char x = timestamp.charAt(4);
char y = timestamp.charAt(13);
if(x == '-' && y == ':') {
tm.Year = (timestamp.substring(0,4).toInt()) - 1970;
tm.Month = timestamp.substring(5,7).toInt();
tm.Day = timestamp.substring(8,10).toInt();
tm.Hour = timestamp.substring(11,13).toInt();
tm.Minute = timestamp.substring(14,16).toInt();
tm.Second = timestamp.substring(17,19).toInt();
}
} else {
meterTimestamp = 0;
}
if(tm.Year > 0) {
meterTimestamp = makeTime(tm);
if(tz != NULL) meterTimestamp = tz->toUTC(meterTimestamp);
}

View File

@@ -9,25 +9,29 @@
#include "Timezone.h"
#include "ntohll.h"
#include "Uptime.h"
#include "hexutils.h"
IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state) {
#if defined(AMS_REMOTE_DEBUG)
IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, RemoteDebug* debugger) {
#else
IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, Stream* debugger) {
#endif
float val;
char str[64];
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
Timezone tz(CEST, CET);
this->packageTimestamp = ctx.timestamp;
this->packageTimestamp = time(nullptr); // ctx.timestamp is mostly garbage, so we use current time as package timestamp
val = getNumber(AMS_OBIS_ACTIVE_IMPORT, sizeof(AMS_OBIS_ACTIVE_IMPORT), ((char *) (d)));
if(val == NOVALUE) {
CosemData* data = getCosemDataAt(1, ((char *) (d)));
// Kaifa special case...
if(data->base.type == CosemTypeOctetString) {
this->packageTimestamp = this->packageTimestamp > 0 ? tz.toUTC(this->packageTimestamp) : 0;
if(useMeterType == AmsTypeKaifa && data->base.type == CosemTypeDLongUnsigned) {
listType = 1;
meterType = AmsTypeKaifa;
activeImportPower = ntohl(data->dlu.data);
lastUpdateMillis = millis64();
} else if(data->base.type == CosemTypeOctetString) {
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
String listId = String(str);
@@ -35,7 +39,7 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
this->listId = listId;
meterType = AmsTypeKaifa;
int idx = 0;
uint8_t idx = 0;
data = getCosemDataAt(idx, ((char *) (d)));
idx+=2;
if(data->base.length == 0x0D || data->base.length == 0x12) {
@@ -116,7 +120,7 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
if(data->oct.length == 0x0C) {
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) data;
time_t ts = decodeCosemDateTime(amst->dt);
meterTimestamp = tz.toUTC(ts);
meterTimestamp = tz != NULL ? tz->toUTC(ts) : ts;
}
}
}
@@ -137,86 +141,577 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
this->listId = listId;
meterType = AmsTypeIskra;
int idx = 0;
uint8_t idx = 0;
data = getCosemDataAt(idx++, ((char *) (d)));
if(data->base.length == 0x12) {
listType = 2;
apply(state);
listType = state.getListType() > 4 ? state.getListType() : 4;
// 42.0.0 COSEM logical device name
idx++;
// 96.1.3 Device ID 4
data = getCosemDataAt(idx++, ((char *) (d)));
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
meterId = String(str);
// 1.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportPower = ntohl(data->dlu.data);
// 2.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportPower = ntohl(data->dlu.data);
// 3.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveImportPower = ntohl(data->dlu.data);
// 4.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveExportPower = ntohl(data->dlu.data);
// 32.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1voltage = ntohs(data->lu.data) / 10.0;
// 52.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2voltage = ntohs(data->lu.data) / 10.0;
// 72.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3voltage = ntohs(data->lu.data) / 10.0;
// 31.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1current = ntohs(data->lu.data) / 100.0;
// 51.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2current = ntohs(data->lu.data) / 100.0;
// 71.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3current = ntohs(data->lu.data) / 100.0;
// 21.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1activeImportPower = ntohl(data->dlu.data);
// 41.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2activeImportPower = ntohl(data->dlu.data);
// 61.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3activeImportPower = ntohl(data->dlu.data);
// 22.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1activeExportPower = ntohl(data->dlu.data);
// 42.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2activeExportPower = ntohl(data->dlu.data);
// 62.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3activeExportPower = ntohl(data->dlu.data);
lastUpdateMillis = millis64();
} else if(data->base.length == 0x0C) {
apply(state);
listType = 3;
idx += 4;
CosemData* no3 = getCosemDataAt(3, ((char *) (d)));
if(no3->base.type == CosemTypeBoolean) {
apply(state);
listType = state.getListType() > 3 ? state.getListType() : 3;
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
idx += 2;
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
idx += 2;
// 42.0.0 COSEM logical device name
idx++;
// 96.1.3 Device ID 4
data = getCosemDataAt(idx++, ((char *) (d)));
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
meterId = String(str);
// 96.3.10 Disconnect control
// 96.14.0 Currently acrive energy tariff
idx += 2;
// 1.8.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
// 1.8.1
// 1.8.2
idx += 2;
// 2.8.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
// 2.8.1
// 2.8.2
idx += 2;
// 3.8.0
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveImportCounter = ntohl(data->dlu.data) / 1000.0;
// 4.8.0
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveExportCounter = ntohl(data->dlu.data) / 1000.0;
lastUpdateMillis = millis64();
} else if(no3->base.type == CosemTypeLongUnsigned) {
apply(state);
listType = state.getListType() > 2 ? state.getListType() : 2;
// 42.0.0 COSEM logical device name
idx++;
// 96.1.2 Device ID 3
data = getCosemDataAt(idx++, ((char *) (d)));
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
meterId = String(str);
// 32.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1voltage = ntohs(data->lu.data) / 10.0;
// 52.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2voltage = ntohs(data->lu.data) / 10.0;
// 72.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3voltage = ntohs(data->lu.data) / 10.0;
// 31.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1current = ntohs(data->lu.data) / 100.0;
// 51.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2current = ntohs(data->lu.data) / 100.0;
// 71.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3current = ntohs(data->lu.data) / 100.0;
// 1.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportPower = ntohl(data->dlu.data);
// 2.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportPower = ntohl(data->dlu.data);
// 3.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveImportPower = ntohl(data->dlu.data);
// 4.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveExportPower = ntohl(data->dlu.data);
lastUpdateMillis = millis64();
}
} else if(data->base.length == 0x0A) {
CosemData* no7 = getCosemDataAt(7, ((char *) (d)));
if(no7->base.type == CosemTypeLongUnsigned) {
apply(state);
listType = state.getListType() > 4 ? state.getListType() : 4;
// 42.0.0 COSEM logical device name
idx++;
// 96.1.2 Device ID 3
data = getCosemDataAt(idx++, ((char *) (d)));
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
meterId = String(str);
// 1.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportPower = ntohl(data->dlu.data);
// 2.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportPower = ntohl(data->dlu.data);
// 3.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveImportPower = ntohl(data->dlu.data);
// 4.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveExportPower = ntohl(data->dlu.data);
// 32.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1voltage = ntohs(data->lu.data) / 10.0;
// 31.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1current = ntohs(data->lu.data) / 100.0;
// 21.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1activeImportPower = ntohl(data->dlu.data);
// 22.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1activeExportPower = ntohl(data->dlu.data);
lastUpdateMillis = millis64();
} else if(no7->base.type == CosemTypeDLongUnsigned) {
apply(state);
listType = state.getListType() > 3 ? state.getListType() : 3;
// 42.0.0 COSEM logical device name
idx++;
// 96.1.3 Device ID 4
data = getCosemDataAt(idx++, ((char *) (d)));
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
meterId = String(str);
// 1.8.1
data = getCosemDataAt(idx++, ((char *) (d)));
double obis181 = ntohl(data->dlu.data) / 1000.0;
// 1.8.2
data = getCosemDataAt(idx++, ((char *) (d)));
double obis182 = ntohl(data->dlu.data) / 1000.0;
if(activeImportCounter < obis181 + obis182)
activeImportCounter = obis181 + obis182;
// 2.8.1
data = getCosemDataAt(idx++, ((char *) (d)));
double obis281 = ntohl(data->dlu.data) / 1000.0;
// 2.8.2
data = getCosemDataAt(idx++, ((char *) (d)));
double obis282 = ntohl(data->dlu.data) / 1000.0;
if(activeExportCounter < obis281 + obis282)
activeExportCounter = obis281 + obis282;
// 3.8.1
data = getCosemDataAt(idx++, ((char *) (d)));
double obis381 = ntohl(data->dlu.data) / 1000.0;
// 3.8.2
data = getCosemDataAt(idx++, ((char *) (d)));
double obis382 = ntohl(data->dlu.data) / 1000.0;
if(reactiveImportCounter < obis381 + obis382)
reactiveImportCounter = obis381 + obis382;
// 4.8.1
data = getCosemDataAt(idx++, ((char *) (d)));
double obis481 = ntohl(data->dlu.data) / 1000.0;
// 4.8.2
data = getCosemDataAt(idx++, ((char *) (d)));
double obis482 = ntohl(data->dlu.data) / 1000.0;
if(reactiveExportCounter < obis481 + obis482)
reactiveExportCounter = obis481 + obis482;
lastUpdateMillis = millis64();
}
} else if(data->base.length == 0x09) {
CosemData* no7 = getCosemDataAt(7, ((char *) (d)));
if(no7->base.type == CosemTypeLongUnsigned) {
apply(state);
listType = state.getListType() > 3 ? state.getListType() : 3;
// 42.0.0 COSEM logical device name
idx++;
// 96.1.2 Device ID 3
data = getCosemDataAt(idx++, ((char *) (d)));
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
meterId = String(str);
// 1.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportPower = ntohl(data->dlu.data);
// 2.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportPower = ntohl(data->dlu.data);
// 3.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveImportPower = ntohl(data->dlu.data);
// 4.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveExportPower = ntohl(data->dlu.data);
// 32.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1voltage = ntohs(data->lu.data) / 10.0;
// 2.8.1
data = getCosemDataAt(idx++, ((char *) (d)));
double obis281 = ntohl(data->dlu.data) / 1000.0;
// 2.8.2
data = getCosemDataAt(idx++, ((char *) (d)));
double obis282 = ntohl(data->dlu.data) / 1000.0;
activeExportCounter = obis281 + obis282;
lastUpdateMillis = millis64();
} else if(no7->base.type == CosemTypeDLongUnsigned) {
apply(state);
listType = state.getListType() > 3 ? state.getListType() : 3;
// 42.0.0 COSEM logical device name?
idx++;
// 1.7.0?
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportPower = ntohl(data->dlu.data);
// 2.7.0?
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportPower = ntohl(data->dlu.data);
// 1.8.0?
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
// 1.8.1?
// 1.8.2?
idx += 2;
// 2.8.0?
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
// 2.8.1?
// 2.8.2?
idx += 2;
lastUpdateMillis = millis64();
}
} else if(data->base.length == 0x08) {
// 42.0.0 COSEM logical device name
idx++;
// 96.1.2 Device ID 3
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveImportCounter = ntohl(data->dlu.data) / 1000.0;
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
meterId = String(str);
// 32.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveExportCounter = ntohl(data->dlu.data) / 1000.0;
l1voltage = ntohs(data->lu.data) / 10.0;
// 31.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1current = ntohs(data->lu.data) / 100.0;
// 1.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportPower = ntohl(data->dlu.data);
// 2.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportPower = ntohl(data->dlu.data);
// 3.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveImportPower = ntohl(data->dlu.data);
// 4.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveExportPower = ntohl(data->dlu.data);
lastUpdateMillis = millis64();
} else if(data->base.length == 0x04) {
// ?
idx++;
// ?
idx++;
// 1.8.0?
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
// 2.8.0?
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
}
} else if(useMeterType == AmsTypeIskra && data->base.type == CosemTypeOctetString) { // Iskra special case
meterType = AmsTypeIskra;
uint8_t idx = 0;
data = getCosemDataAt(idx, ((char *) (d)));
if(data->base.length == 0x21) {
idx = 4;
// 1.8.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
// 1.8.1
// 1.8.2
idx += 2;
// 2.8.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
// 2.8.1
// 2.8.2
idx += 2;
// 5.8.0
// 6.8.0
// 7.8.0
// 8.8.0
idx += 4;
// 1.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportPower = ntohl(data->dlu.data);
// 2.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportPower = ntohl(data->dlu.data);
// 13.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
powerFactor= ntohl(data->dlu.data) / 1000.0;
// 21.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1activeImportPower = ntohl(data->dlu.data);
// 41.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2activeImportPower = ntohl(data->dlu.data);
// 61.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3activeImportPower = ntohl(data->dlu.data);
// 22.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1activeExportPower = ntohl(data->dlu.data);
// 42.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2activeExportPower = ntohl(data->dlu.data);
// 62.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3activeExportPower = ntohl(data->dlu.data);
// 32.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1voltage = ntohs(data->lu.data) / 10.0;
// 52.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2voltage = ntohs(data->lu.data) / 10.0;
// 72.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3voltage = ntohs(data->lu.data) / 10.0;
// 31.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1current = ntohs(data->lu.data) / 100.0;
// 51.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2current = ntohs(data->lu.data) / 100.0;
// 71.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3current = ntohs(data->lu.data) / 100.0;
listType = 4;
lastUpdateMillis = millis64();
} else {
idx = 5;
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
reactiveImportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
reactiveExportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeImportPower = ntohl(data->dlu.data);
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeExportPower = ntohl(data->dlu.data);
}
uint8_t str_len = 0;
str_len = getString(AMS_OBIS_UNKNOWN_1, sizeof(AMS_OBIS_UNKNOWN_1), ((char *) (d)), str);
if(str_len > 0) {
meterId = String(str);
}
listType = 4;
lastUpdateMillis = millis64();
}
} else if(useMeterType == AmsTypeUnknown) {
uint8_t idx = 1;
CosemData* d1 = getCosemDataAt(idx++, ((char *) (d)));
CosemData* d2 = getCosemDataAt(idx++, ((char *) (d)));
CosemData* d3 = getCosemDataAt(idx++, ((char *) (d)));
if(d1->base.type == CosemTypeOctetString && d2->base.type == CosemTypeOctetString && d3->base.type == CosemTypeOctetString) {
meterType = AmsTypeIskra;
lastUpdateMillis = millis64();
listType = 3;
} else {
uint8_t str_len = 0;
str_len = getString(AMS_OBIS_UNKNOWN_1, sizeof(AMS_OBIS_UNKNOWN_1), ((char *) (d)), str);
if(str_len > 0) {
meterType = AmsTypeIskra;
meterId = String(str);
lastUpdateMillis = millis64();
listType = 3;
}
}
}
} else if(useMeterType == AmsTypeKaifa && data->base.type == CosemTypeDLongUnsigned) {
this->packageTimestamp = this->packageTimestamp > 0 ? tz.toUTC(this->packageTimestamp) : 0;
listType = 1;
meterType = AmsTypeKaifa;
activeImportPower = ntohl(data->dlu.data);
lastUpdateMillis = millis64();
}
// Kaifa end
} else {
listType = 1;
activeImportPower = val;
@@ -239,20 +734,6 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
}
}
}
// Try system title
if(meterType == AmsTypeUnknown) {
if(memcmp(ctx.system_title, "SAGY", 4) == 0) {
meterType = AmsTypeSagemcom;
} else if(memcmp(ctx.system_title, "KFM", 3) == 0) {
meterType = AmsTypeKaifa;
}
}
if(this->packageTimestamp > 0) {
if(meterType == AmsTypeAidon || meterType == AmsTypeKamstrup) {
this->packageTimestamp = this->packageTimestamp - 3600;
}
}
uint8_t str_len = 0;
str_len = getString(AMS_OBIS_VERSION, sizeof(AMS_OBIS_VERSION), ((char *) (d)), str);
@@ -353,12 +834,7 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
CosemData* meterTs = findObis(AMS_OBIS_METER_TIMESTAMP, sizeof(AMS_OBIS_METER_TIMESTAMP), ((char *) (d)));
if(meterTs != NULL) {
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs;
time_t ts = decodeCosemDateTime(amst->dt);
if(meterType == AmsTypeAidon || meterType == AmsTypeKamstrup) {
meterTimestamp = ts - 3600;
} else {
meterTimestamp = ts;
}
this->meterTimestamp = adjustForKnownIssues(amst->dt, tz, meterType == AmsTypeUnknown ? useMeterType : meterType);
}
val = getNumber(AMS_OBIS_POWER_FACTOR, sizeof(AMS_OBIS_POWER_FACTOR), ((char *) (d)));
@@ -501,6 +977,22 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
lastUpdateMillis = millis64();
}
// Try system title
if(meterType == AmsTypeUnknown) {
if(memcmp(ctx.system_title, "SAGY", 4) == 0) {
meterType = AmsTypeSagemcom;
} else if(memcmp(ctx.system_title, "KFM", 3) == 0) {
meterType = AmsTypeKaifa;
} else if(memcmp(ctx.system_title, "ISK", 3) == 0) {
meterType = AmsTypeIskra;
}
if(meterId.isEmpty() && meterType != AmsTypeUnknown) {
stripNonAscii((uint8_t*) ctx.system_title, 8);
meterId = String((const char*)ctx.system_title);
}
}
if(meterConfig->wattageMultiplier > 0) {
activeImportPower = activeImportPower > 0 ? activeImportPower * (meterConfig->wattageMultiplier / 1000.0) : 0;
activeExportPower = activeExportPower > 0 ? activeExportPower * (meterConfig->wattageMultiplier / 1000.0) : 0;
@@ -522,6 +1014,12 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
activeExportCounter = activeExportCounter > 0 ? activeExportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
reactiveImportCounter = reactiveImportCounter > 0 ? reactiveImportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
reactiveExportCounter = reactiveExportCounter > 0 ? reactiveExportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
l1activeImportCounter = l1activeImportCounter > 0 ? l1activeImportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
l2activeImportCounter = l2activeImportCounter > 0 ? l2activeImportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
l3activeImportCounter = l3activeImportCounter > 0 ? l3activeImportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
l1activeExportCounter = l1activeExportCounter > 0 ? l1activeExportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
l2activeExportCounter = l2activeExportCounter > 0 ? l2activeExportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
l3activeExportCounter = l3activeExportCounter > 0 ? l3activeExportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
}
threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0;
@@ -535,6 +1033,7 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
threePhase = true;
}
}
meterId.trim();
}
CosemData* IEC6205675::getCosemDataAt(uint8_t index, const char* ptr) {
@@ -711,3 +1210,24 @@ time_t IEC6205675::getTimestamp(uint8_t* obis, int matchlength, const char* ptr)
}
return 0;
}
time_t IEC6205675::adjustForKnownIssues(CosemDateTime dt, Timezone* tz, uint8_t meterType) {
time_t ts = decodeCosemDateTime(dt);
int16_t deviation = ntohs(dt.deviation);
if(deviation < -720 || deviation > 720) {
// Time zone not specified
if(meterType == AmsTypeAidon || meterType == AmsTypeKamstrup) {
// Special known case
// 21.09.24, the clock is now correct for Aidon
// 23.10.25, the clock is now correct for Kamstrup
ts -= 3600;
} else if(tz != NULL) {
// Adjust from localtime to UTC
ts = tz->toUTC(ts);
}
} else if(meterType == AmsTypeAidon) {
// 21.09.24, the clock is now correct for Aidon
ts -= 3600;
}
return ts;
}

View File

@@ -1,3 +1,9 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "ImpulseAmsData.h"
#include "Uptime.h"

View File

@@ -0,0 +1,92 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "KmpCommunicator.h"
#include "Uptime.h"
#include "crc.h"
#include "OBIScodes.h"
#include "hexutils.h"
void KmpCommunicator::configure(MeterConfig& meterConfig) {
this->meterConfig = meterConfig;
this->configChanged = false;
if(meterConfig.baud == 0) {
this->configChanged = true;
meterConfig.baud = 9600;
meterConfig.parity = 7;
meterConfig.invert = false;
}
setupHanPort(meterConfig.baud, meterConfig.parity, meterConfig.invert, false);
talker = new KmpTalker(hanSerial, hanBuffer, hanBufferSize);
}
bool KmpCommunicator::loop() {
uint64_t now = millis64();
bool ret = talker->loop();
int lastError = getLastError();
if(ret) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("Successful loop\n"));
Serial.flush();
} else if(lastError < 0 && lastError != DATA_PARSE_INCOMPLETE) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Error code: %d\n"), getLastError());
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
{
debugger->printf_P(PSTR(" payload:\n"));
debugPrint(hanBuffer, 0, hanBufferSize, debugger);
}
}
return ret;
}
int KmpCommunicator::getLastError() {
return talker == NULL ? DATA_PARSE_FAIL : talker->getLastError();
}
AmsData* KmpCommunicator::getData(AmsData& meterState) {
if(talker == NULL) return NULL;
KmpDataHolder kmpData;
talker->getData(kmpData);
AmsData* data = new AmsData();
data->apply(OBIS_ACTIVE_IMPORT_COUNT, kmpData.activeImportCounter);
data->apply(OBIS_ACTIVE_EXPORT_COUNT, kmpData.activeExportCounter);
data->apply(OBIS_REACTIVE_IMPORT_COUNT, kmpData.reactiveImportCounter);
data->apply(OBIS_REACTIVE_EXPORT_COUNT, kmpData.reactiveExportCounter);
data->apply(OBIS_ACTIVE_IMPORT, kmpData.activeImportPower);
data->apply(OBIS_ACTIVE_EXPORT, kmpData.activeExportPower);
data->apply(OBIS_REACTIVE_IMPORT, kmpData.reactiveImportPower);
data->apply(OBIS_REACTIVE_EXPORT, kmpData.reactiveExportPower);
data->apply(OBIS_VOLTAGE_L1, kmpData.l1voltage);
data->apply(OBIS_VOLTAGE_L2, kmpData.l2voltage);
data->apply(OBIS_VOLTAGE_L3, kmpData.l3voltage);
data->apply(OBIS_CURRENT_L1, kmpData.l1current);
data->apply(OBIS_CURRENT_L2, kmpData.l2current);
data->apply(OBIS_CURRENT_L3, kmpData.l3current);
data->apply(OBIS_POWER_FACTOR_L1, kmpData.l1PowerFactor);
data->apply(OBIS_POWER_FACTOR_L2, kmpData.l2PowerFactor);
data->apply(OBIS_POWER_FACTOR_L3, kmpData.l3PowerFactor);
data->apply(OBIS_POWER_FACTOR, kmpData.powerFactor);
data->apply(OBIS_ACTIVE_IMPORT_L1, kmpData.l1activeImportPower);
data->apply(OBIS_ACTIVE_IMPORT_L2, kmpData.l2activeImportPower);
data->apply(OBIS_ACTIVE_IMPORT_L3, kmpData.l3activeImportPower);
data->apply(OBIS_ACTIVE_EXPORT_L1, kmpData.l1activeExportPower);
data->apply(OBIS_ACTIVE_EXPORT_L2, kmpData.l2activeExportPower);
data->apply(OBIS_ACTIVE_EXPORT_L3, kmpData.l3activeExportPower);
data->apply(OBIS_ACTIVE_IMPORT_COUNT_L1, kmpData.l1activeImportCounter);
data->apply(OBIS_ACTIVE_IMPORT_COUNT_L2, kmpData.l2activeImportCounter);
data->apply(OBIS_ACTIVE_IMPORT_COUNT_L3, kmpData.l3activeImportCounter);
data->apply(OBIS_METER_ID, kmpData.meterId);
data->apply(OBIS_NULL, AmsTypeKamstrup);
return data;
}

View File

@@ -9,7 +9,7 @@
#include "ntohll.h"
#include "Uptime.h"
LNG::LNG(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, RemoteDebug* debugger) {
LNG::LNG(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx) {
LngHeader* h = (LngHeader*) payload;
if(h->tag == CosemTypeStructure && h->arrayTag == CosemTypeArray) {
apply(meterState);
@@ -42,33 +42,75 @@ LNG::LNG(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterCo
switch(descriptor->obis[2]) {
case 1:
o170 = getNumber(item);
if(meterConfig->wattageMultiplier > 0) {
o170 = o170 > 0 ? o170 * (meterConfig->wattageMultiplier / 1000.0) : 0;
}
break;
case 2:
o270 = getNumber(item);
if(meterConfig->wattageMultiplier > 0) {
o270 = o270 > 0 ? o270 * (meterConfig->wattageMultiplier / 1000.0) : 0;
}
break;
case 3:
reactiveImportPower = getNumber(item);
if(meterConfig->wattageMultiplier > 0) {
reactiveImportPower = reactiveImportPower > 0 ? reactiveImportPower * (meterConfig->wattageMultiplier / 1000.0) : 0;
}
break;
case 4:
reactiveExportPower = getNumber(item);
if(meterConfig->wattageMultiplier > 0) {
reactiveExportPower = reactiveExportPower > 0 ? reactiveExportPower * (meterConfig->wattageMultiplier / 1000.0) : 0;
}
break;
case 21:
l1activeImportPower = getNumber(item);
listType = listType >= 4 ? listType : 4;
break;
case 41:
l2activeImportPower = getNumber(item);
listType = listType >= 4 ? listType : 4;
break;
case 61:
l3activeImportPower = getNumber(item);
listType = listType >= 4 ? listType : 4;
break;
case 31:
l1current = getNumber(item) / 100.0;
if(meterConfig->amperageMultiplier > 0) {
l1current = l1current > 0 ? l1current * (meterConfig->amperageMultiplier / 1000.0) : 0;
}
break;
case 51:
l2current = getNumber(item) / 100.0;
if(meterConfig->amperageMultiplier > 0) {
l2current = l2current > 0 ? l2current * (meterConfig->amperageMultiplier / 1000.0) : 0;
}
break;
case 71:
l3current = getNumber(item) / 100.0;
if(meterConfig->amperageMultiplier > 0) {
l3current = l3current > 0 ? l3current * (meterConfig->amperageMultiplier / 1000.0) : 0;
}
break;
case 32:
l1voltage = getNumber(item) / 10.0;
if(meterConfig->voltageMultiplier > 0) {
l1voltage = l1voltage > 0 ? l1voltage * (meterConfig->voltageMultiplier / 1000.0) : 0;
}
break;
case 52:
l2voltage = getNumber(item) / 10.0;
if(meterConfig->voltageMultiplier > 0) {
l2voltage = l2voltage > 0 ? l2voltage * (meterConfig->voltageMultiplier / 1000.0) : 0;
}
break;
case 72:
l3voltage = getNumber(item) / 10.0;
if(meterConfig->voltageMultiplier > 0) {
l3voltage = l3voltage > 0 ? l3voltage * (meterConfig->voltageMultiplier / 1000.0) : 0;
}
break;
}
}
@@ -79,30 +121,54 @@ LNG::LNG(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterCo
case 1:
o180 = getNumber(item);
activeImportCounter = o180 / 1000.0;
if(meterConfig->accumulatedMultiplier > 0) {
activeImportCounter = activeImportCounter > 0 ? activeImportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
}
break;
case 2:
o280 = getNumber(item);
activeExportCounter = o280 / 1000.0;
if(meterConfig->accumulatedMultiplier > 0) {
activeExportCounter = activeExportCounter > 0 ? activeExportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
}
break;
case 3:
o380 = getNumber(item);
reactiveImportCounter = o380 / 1000.0;
if(meterConfig->accumulatedMultiplier > 0) {
reactiveImportCounter = reactiveImportCounter > 0 ? reactiveImportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
}
break;
case 4:
o480 = getNumber(item);
reactiveExportCounter = o480 / 1000.0;
if(meterConfig->accumulatedMultiplier > 0) {
reactiveExportCounter = reactiveExportCounter > 0 ? reactiveExportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
}
break;
case 5:
o580 = getNumber(item);
if(meterConfig->accumulatedMultiplier > 0) {
o580 = o580 > 0 ? o580 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
}
break;
case 6:
o680 = getNumber(item);
if(meterConfig->accumulatedMultiplier > 0) {
o680 = o680 > 0 ? o680 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
}
break;
case 7:
o780 = getNumber(item);
if(meterConfig->accumulatedMultiplier > 0) {
o780 = o780 > 0 ? o780 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
}
break;
case 8:
o880 = getNumber(item);
if(meterConfig->accumulatedMultiplier > 0) {
o880 = o880 > 0 ? o880 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
}
break;
}
} else if(descriptor->obis[4] == 1) {
@@ -110,9 +176,15 @@ LNG::LNG(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterCo
switch(descriptor->obis[2]) {
case 1:
o181 = getNumber(item);
if(meterConfig->accumulatedMultiplier > 0) {
o181 = o181 > 0 ? o181 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
}
break;
case 2:
o281 = getNumber(item);
if(meterConfig->accumulatedMultiplier > 0) {
o281 = o281 > 0 ? o281 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
}
break;
}
} else if(descriptor->obis[4] == 2) {
@@ -120,9 +192,15 @@ LNG::LNG(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterCo
switch(descriptor->obis[2]) {
case 1:
o182 = getNumber(item);
if(meterConfig->accumulatedMultiplier > 0) {
o182 = o182 > 0 ? o182 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
}
break;
case 2:
o282 = getNumber(item);
if(meterConfig->accumulatedMultiplier > 0) {
o282 = o282 > 0 ? o282 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
}
break;
}
}
@@ -182,6 +260,11 @@ LNG::LNG(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterCo
lastUpdateMillis = millis64();
}
lastUpdateMillis = millis64();
threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0;
if(!threePhase)
twoPhase = (l1voltage > 0 && l2voltage > 0) || (l2voltage > 0 && l3voltage > 0) || (l3voltage > 0 && l1voltage > 0);
}
}

View File

@@ -7,14 +7,40 @@
#include "LNG2.h"
#include "Uptime.h"
LNG2::LNG2(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, RemoteDebug* debugger) {
LNG2::LNG2(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx) {
CosemBasic* h = (CosemBasic*) payload;
if(h->length == 0x0e) {
apply(meterState);
meterType = AmsTypeLandisGyr;
this->packageTimestamp = ctx.timestamp;
Lng2Data_3p* d = (Lng2Data_3p*) payload;
Lng2Data_3p_0e* d = (Lng2Data_3p_0e*) payload;
this->l1voltage = ntohs(d->u1.data);
this->l2voltage = ntohs(d->u2.data);
this->l3voltage = ntohs(d->u3.data);
this->l1current = ntohs(d->i1.data) / 100.0;
this->l2current = ntohs(d->i2.data) / 100.0;
this->l3current = ntohs(d->i3.data) / 100.0;
this->activeImportPower = ntohl(d->activeImport.data);
this->activeExportPower = ntohl(d->activeExport.data);
this->activeImportCounter = ntohl(d->acumulatedImport.data) / 1000.0;
this->activeExportCounter = ntohl(d->accumulatedExport.data) / 1000.0;
char str[64];
uint8_t str_len = getString((CosemData*) &d->meterId, str);
if(str_len > 0) {
this->meterId = String(str);
}
listType = 3;
lastUpdateMillis = millis64();
} else if(h->length == 0x0b) {
apply(meterState);
meterType = AmsTypeLandisGyr;
this->packageTimestamp = ctx.timestamp;
Lng2Data_3p_0b* d = (Lng2Data_3p_0b*) payload;
this->l1voltage = ntohs(d->u1.data);
this->l2voltage = ntohs(d->u2.data);
this->l3voltage = ntohs(d->u3.data);

View File

@@ -0,0 +1,905 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "PassiveMeterCommunicator.h"
#include "IEC6205675.h"
#include "IEC6205621.h"
#include "LNG.h"
#include "LNG2.h"
#include "hexutils.h"
#if defined(ESP32)
#include <driver/uart.h>
#endif
#if defined(AMS_REMOTE_DEBUG)
PassiveMeterCommunicator::PassiveMeterCommunicator(RemoteDebug* debugger) {
this->debugger = debugger;
}
#else
PassiveMeterCommunicator::PassiveMeterCommunicator(Stream* debugger) {
this->debugger = debugger;
}
#endif
void PassiveMeterCommunicator::configure(MeterConfig& meterConfig, Timezone* tz) {
this->meterConfig = meterConfig;
if(meterConfig.baud == 0) {
autodetect = true;
}
this->configChanged = false;
this->tz = tz;
setupHanPort(meterConfig.baud, meterConfig.parity, meterConfig.invert);
if(dsmrParser != NULL) {
delete dsmrParser;
dsmrParser = NULL;
}
if(gcmParser != NULL) {
delete gcmParser;
gcmParser = NULL;
}
bool encen = false;
for(uint8_t i = 0; i < 16; i++) {
if(meterConfig.encryptionKey[i] > 0) {
encen = true;
}
}
if(encen) {
gcmParser = new GCMParser(meterConfig.encryptionKey, meterConfig.authenticationKey);
}
}
bool PassiveMeterCommunicator::loop() {
if(hanBufferSize == 0) return false;
unsigned long now = millis();
if(autodetect) handleAutodetect(now);
unsigned long start, end;
if(!hanSerial->available()) {
return false;
}
// Before reading, empty serial buffer to increase chance of getting first byte of a data transfer
if(!serialInit) {
hanSerial->readBytes(hanBuffer, hanBufferSize);
serialInit = true;
return false;
}
dataAvailable = false;
ctx = {0,0,0,0};
memset(ctx.system_title, 0, 8);
pos = DATA_PARSE_INCOMPLETE;
// For each byte received, check if we have a complete frame we can handle
start = millis();
while(hanSerial->available() && pos == DATA_PARSE_INCOMPLETE) {
// If buffer was overflowed, reset
if(len >= hanBufferSize) {
hanSerial->readBytes(hanBuffer, hanBufferSize);
len = 0;
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Buffer overflow, resetting\n"));
return false;
}
hanBuffer[len++] = hanSerial->read();
ctx.length = len;
pos = unwrapData((uint8_t *) hanBuffer, ctx);
if(ctx.type > 0 && pos >= 0) {
switch(ctx.type) {
case DATA_TAG_DLMS:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Received valid DLMS at %d +%d\n"), pos, ctx.length);
break;
case DATA_TAG_DSMR:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Received valid DSMR at %d +%d\n"), pos, ctx.length);
break;
case DATA_TAG_SNRM:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Received valid SNMR at %d +%d\n"), pos, ctx.length);
break;
case DATA_TAG_AARE:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Received valid AARE at %d +%d\n"), pos, ctx.length);
break;
case DATA_TAG_RES:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Received valid Get Response at %d +%d\n"), pos, ctx.length);
break;
case DATA_TAG_HDLC:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Received valid HDLC at %d +%d\n"), pos, ctx.length);
break;
default:
// TODO: Move this so that payload is sent to MQTT
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("Unknown tag %02X at pos %d\n"), ctx.type, pos);
len = 0;
return false;
}
}
yield();
}
end = millis();
if(end-start > 1000) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Used %dms to unwrap HAN data\n"), end-start);
}
if(pos == DATA_PARSE_INCOMPLETE) {
return false;
} else if(pos == DATA_PARSE_UNKNOWN_DATA) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Unknown data received\n"));
lastError = pos;
len = len + hanSerial->readBytes(hanBuffer+len, hanBufferSize-len);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
{
debugger->printf_P(PSTR(" payload:\n"));
debugPrint(hanBuffer, 0, len, debugger);
}
len = 0;
return false;
}
if(pos == DATA_PARSE_INTERMEDIATE_SEGMENT) {
len = 0;
return false;
} else if(pos < 0) {
lastError = pos;
printHanReadError(pos);
len += hanSerial->readBytes(hanBuffer+len, hanBufferSize-len);
if(mqttDebug != NULL) {
mqttDebug->publishRaw(hanBuffer, len);
}
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
{
debugger->printf_P(PSTR(" payload:\n"));
debugPrint(hanBuffer, 0, len, debugger);
}
while(hanSerial->available()) hanSerial->read(); // Make sure it is all empty, in case we overflowed buffer above
len = 0;
return false;
}
if(ctx.type == 0) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Ended up with context type %d, return code %d and length: %lu/%lu\n"), ctx.type, pos, ctx.length, len);
lastError = pos;
len = len + hanSerial->readBytes(hanBuffer+len, hanBufferSize-len);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
{
debugger->printf_P(PSTR(" payload:\n"));
debugPrint(hanBuffer, 0, len, debugger);
}
len = 0;
return false;
}
// Data is valid, clear the rest of the buffer to avoid tainted parsing
for(int i = pos+ctx.length; i<hanBufferSize; i++) {
hanBuffer[i] = 0x00;
}
dataAvailable = true;
lastError = DATA_PARSE_OK;
return true;
}
AmsData* PassiveMeterCommunicator::getData(AmsData& meterState) {
if(!dataAvailable) return NULL;
if(ctx.length > hanBufferSize) {
debugger->printf_P(PSTR("Invalid context length\n"));
dataAvailable = false;
return NULL;
}
AmsData* data = NULL;
char* payload = ((char *) (hanBuffer)) + pos;
if(maxDetectedPayloadSize < pos) maxDetectedPayloadSize = pos;
if(ctx.type == DATA_TAG_DLMS) {
if(mqttDebug != NULL) {
mqttDebug->publishRaw((uint8_t*) payload, ctx.length);
}
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("Using application data:\n"));
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugPrint((byte*) payload, 0, ctx.length, debugger);
// Rudimentary detector for L&G proprietary format, this is terrible code... Fix later
if(payload[0] == CosemTypeStructure && payload[2] == CosemTypeArray && payload[1] == payload[3]) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("LNG\n"));
LNG lngData = LNG(meterState, payload, meterState.getMeterType(), &meterConfig, ctx);
if(lngData.getListType() >= 1) {
data = new AmsData();
data->apply(meterState);
data->apply(lngData);
}
} else if(payload[0] == CosemTypeStructure &&
payload[2] == CosemTypeLongUnsigned &&
payload[5] == CosemTypeLongUnsigned &&
payload[8] == CosemTypeLongUnsigned &&
payload[11] == CosemTypeLongUnsigned &&
payload[14] == CosemTypeLongUnsigned &&
payload[17] == CosemTypeLongUnsigned
) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("LNG2\n"));
LNG2 lngData = LNG2(meterState, payload, meterState.getMeterType(), &meterConfig, ctx);
if(lngData.getListType() >= 1) {
data = new AmsData();
data->apply(meterState);
data->apply(lngData);
}
} else {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("DLMS\n"));
// TODO: Split IEC6205675 into DataParserKaifa and DataParserObis. This way we can add other means of parsing, for those other proprietary formats
data = new IEC6205675(payload, tz, meterState.getMeterType(), &meterConfig, ctx, meterState, debugger);
}
} else if(ctx.type == DATA_TAG_DSMR) {
data = new IEC6205621(payload, tz, &meterConfig);
}
len = 0;
if(data != NULL) {
if(data->getListType() > 0) {
validDataReceived++;
if(rxBufferErrors > 0) rxBufferErrors--;
}
}
dataAvailable = false;
return data;
}
int PassiveMeterCommunicator::getLastError() {
#if defined ESP8266
if(hwSerial != NULL) {
if(hwSerial->hasRxError()) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("Serial RX error\n"));
lastError = 96;
}
if(hwSerial->hasOverrun()) {
rxerr(2);
}
} else if(swSerial != NULL) {
if(swSerial->overflow()) {
rxerr(2);
}
}
#endif
return lastError;
}
bool PassiveMeterCommunicator::isConfigChanged() {
return configChanged;
}
void PassiveMeterCommunicator::ackConfigChanged() {
configChanged = false;
}
void PassiveMeterCommunicator::getCurrentConfig(MeterConfig& meterConfig) {
meterConfig = this->meterConfig;
}
int16_t PassiveMeterCommunicator::unwrapData(uint8_t *buf, DataParserContext &context) {
int16_t ret = 0;
bool doRet = false;
uint16_t end = hanBufferSize;
uint8_t tag = (*buf);
uint8_t lastTag = DATA_TAG_NONE;
while(tag != DATA_TAG_NONE) {
int16_t curLen = context.length;
int8_t res = 0;
switch(tag) {
case DATA_TAG_HDLC:
if(hdlcParser == NULL) hdlcParser = new HDLCParser();
res = hdlcParser->parse(buf, context);
if(context.length < 3) doRet = true;
break;
case DATA_TAG_MBUS:
if(mbusParser == NULL) mbusParser = new MBUSParser();
res = mbusParser->parse(buf, context);
break;
case DATA_TAG_GBT:
if(gbtParser == NULL) gbtParser = new GBTParser();
res = gbtParser->parse(buf, context);
break;
case DATA_TAG_GCM:
if(gcmParser == NULL) gcmParser = new GCMParser(meterConfig.encryptionKey, meterConfig.authenticationKey);
res = gcmParser->parse(buf, context);
break;
case DATA_TAG_LLC:
if(llcParser == NULL) llcParser = new LLCParser();
res = llcParser->parse(buf, context);
break;
case DATA_TAG_DLMS:
if(dlmsParser == NULL) dlmsParser = new DLMSParser();
res = dlmsParser->parse(buf, context);
if(res >= 0) doRet = true;
break;
case DATA_TAG_DSMR:
if(dsmrParser == NULL) dsmrParser = new DSMRParser(gcmParser);
#if defined(AMS_REMOTE_DEBUG)
res = dsmrParser->parse(buf, context, lastTag != DATA_TAG_NONE, debugger->isActive(RemoteDebug::VERBOSE) ? debugger : NULL);
#else
res = dsmrParser->parse(buf, context, lastTag != DATA_TAG_NONE, debugger);
#endif
if(res >= 0) doRet = true;
break;
case DATA_TAG_SNRM:
case DATA_TAG_AARE:
case DATA_TAG_RES:
res = DATA_PARSE_OK;
doRet = true;
break;
default:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("Ended up in default case while unwrapping...(tag %02X)\n"), tag);
return DATA_PARSE_UNKNOWN_DATA;
}
lastTag = tag;
if(res == DATA_PARSE_INCOMPLETE) {
return res;
}
if(context.length > end) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("Context length %lu > %lu:\n"), context.length, end);
context.type = 0;
context.length = 0;
return false;
}
switch(tag) {
case DATA_TAG_HDLC:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("HDLC frame:\n"));
if(mqttDebug != NULL) {
mqttDebug->publishRaw(buf, curLen);
}
break;
case DATA_TAG_MBUS:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("MBUS frame:\n"));
if(mqttDebug != NULL) {
mqttDebug->publishRaw(buf, curLen);
}
break;
case DATA_TAG_GBT:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("GBT frame:\n"));
break;
case DATA_TAG_GCM:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("GCM frame:\n"));
break;
case DATA_TAG_LLC:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("LLC frame:\n"));
break;
case DATA_TAG_DLMS:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("DLMS frame:\n"));
break;
case DATA_TAG_DSMR:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("DSMR frame:\n"));
if(mqttDebug != NULL) {
mqttDebug->publishRaw(buf, curLen);
}
break;
case DATA_TAG_SNRM:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("SNMR frame:\n"));
break;
case DATA_TAG_AARE:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("AARE frame:\n"));
break;
case DATA_TAG_RES:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("RES frame:\n"));
break;
}
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugPrint(buf, 0, curLen, debugger);
if(res == DATA_PARSE_FINAL_SEGMENT) {
if(tag == DATA_TAG_MBUS) {
res = mbusParser->write(buf, context);
}
}
if(res < 0) {
return res;
}
buf += res;
end -= res;
ret += res;
// If we are ready to return, do that
if(doRet) {
context.type = tag;
return ret;
}
// Use start byte of new buffer position as tag for next round in loop
tag = (*buf);
}
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("Got to end of unwrap method...\n"));
return DATA_PARSE_UNKNOWN_DATA;
}
void PassiveMeterCommunicator::printHanReadError(int pos) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
{
switch(pos) {
case DATA_PARSE_BOUNDARY_FLAG_MISSING:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Boundary flag missing\n"));
break;
case DATA_PARSE_HEADER_CHECKSUM_ERROR:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Header checksum error\n"));
break;
case DATA_PARSE_FOOTER_CHECKSUM_ERROR:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Frame checksum error\n"));
break;
case DATA_PARSE_INCOMPLETE:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Received frame is incomplete\n"));
break;
case GCM_AUTH_FAILED:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Decrypt authentication failed\n"));
break;
case GCM_ENCRYPTION_KEY_FAILED:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Setting decryption key failed\n"));
break;
case GCM_DECRYPT_FAILED:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Decryption failed\n"));
break;
case MBUS_FRAME_LENGTH_NOT_EQUAL:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Frame length mismatch\n"));
break;
case DATA_PARSE_INTERMEDIATE_SEGMENT:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Intermediate segment received\n"));
break;
case DATA_PARSE_UNKNOWN_DATA:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Unknown data format %02X\n"), hanBuffer[0]);
break;
default:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Unspecified error while reading data: %d\n"), pos);
}
}
}
void PassiveMeterCommunicator::setupHanPort(uint32_t baud, uint8_t parityOrdinal, bool invert, bool passive) {
int8_t rxpin = meterConfig.rxPin;
int8_t txpin = passive ? -1 : meterConfig.txPin;
if(baud == 0) {
autodetectBaud = baud = 2400;
}
if(parityOrdinal == 0) {
parityOrdinal = 11; // 8E1
}
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("(setupHanPort) Setting up HAN on pin %d/%d with baud %d and parity %d\n"), rxpin, txpin, baud, parityOrdinal);
if(rxpin == 3 || rxpin == 113) {
#if ARDUINO_USB_CDC_ON_BOOT
hwSerial = &Serial0;
#else
hwSerial = &Serial;
#endif
}
uint8_t uart_num = 0;
#if defined(ESP32)
hwSerial = &Serial1;
uart_num = UART_NUM_1;
#if defined(CONFIG_IDF_TARGET_ESP32)
if(rxpin == 16) {
hwSerial = &Serial2;
uart_num = UART_NUM_2;
}
#endif
#endif
if(rxpin == 0) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("Invalid GPIO configured for HAN\n"));
return;
}
if(meterConfig.bufferSize < 1) meterConfig.bufferSize = 1;
if(meterConfig.bufferSize > 64) meterConfig.bufferSize = 64;
if(hwSerial != NULL) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Hardware serial\n"));
Serial.flush();
#if defined(ESP8266)
SerialConfig serialConfig;
#elif defined(ESP32)
uint32_t serialConfig;
#endif
switch(parityOrdinal) {
case 2:
serialConfig = SERIAL_7N1;
break;
case 3:
serialConfig = SERIAL_8N1;
break;
case 7:
serialConfig = SERIAL_8N2;
break;
case 10:
serialConfig = SERIAL_7E1;
break;
default:
serialConfig = SERIAL_8E1;
break;
}
if(meterConfig.bufferSize < 4) meterConfig.bufferSize = 4; // 64 bytes (1) is default for software serial, 256 bytes (4) for hardware
hwSerial->setRxBufferSize(64 * meterConfig.bufferSize);
#if defined(ESP32)
hwSerial->begin(baud, serialConfig, -1, -1, invert);
uart_set_pin(uart_num, txpin, rxpin, -1, -1);
#else
hwSerial->begin(baud, serialConfig, SERIAL_FULL, 1, invert);
#endif
#if defined(ESP8266)
if(rxpin == 3) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Switching UART0 to pin 1 & 3\n"));
Serial.pins(1,3);
} else if(rxpin == 113) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Switching UART0 to pin 15 & 13\n"));
Serial.pins(15,13);
}
#endif
// Prevent pullup on TX pin if not uart0
#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)
if(txpin != 17) pinMode(17, INPUT);
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
if(txpin != 7) pinMode(7, INPUT);
#elif defined(ESP32)
if(rxpin == 9 && txpin != 10) {
pinMode(10, INPUT);
} else if(rxpin == 16 && txpin != 17) {
pinMode(17, INPUT);
}
#elif defined(ESP8266)
if(rxpin == 113) {
pinMode(15, INPUT);
}
#endif
hanSerial = hwSerial;
#if defined(ESP8266)
if(swSerial != NULL) {
swSerial->end();
delete swSerial;
swSerial = NULL;
}
#endif
} else {
#if defined(ESP8266)
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Software serial\n"));
Serial.flush();
if(swSerial == NULL) {
swSerial = new SoftwareSerial(rxpin, txpin, invert);
} else {
swSerial->end();
}
SoftwareSerialConfig serialConfig;
switch(parityOrdinal) {
case 2:
serialConfig = SWSERIAL_7N1;
break;
case 3:
serialConfig = SWSERIAL_8N1;
break;
case 7:
serialConfig = SWSERIAL_8N2;
break;
case 10:
serialConfig = SWSERIAL_7E1;
break;
default:
serialConfig = SWSERIAL_8E1;
break;
}
uint8_t bufferSize = meterConfig.bufferSize;
#if defined(ESP8266)
if(bufferSize > 2) bufferSize = 2;
#endif
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Using serial buffer size %d\n"), 64 * bufferSize);
swSerial->begin(baud, serialConfig, rxpin, txpin, invert, meterConfig.bufferSize * 64, meterConfig.bufferSize * 64);
hanSerial = swSerial;
hwSerial = NULL;
#else
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Software serial not available\n"));
return;
#endif
}
if(hanBuffer != NULL) {
free(hanBuffer);
}
hanBufferSize = max(64 * meterConfig.bufferSize * 3, 512);
hanBuffer = (uint8_t*) malloc(hanBufferSize);
// The library automatically sets the pullup in Serial.begin()
if(!meterConfig.rxPinPullup) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("HAN pin pullup disabled\n"));
pinMode(meterConfig.rxPin, INPUT);
}
if(meterConfig.txPin != 0xFF && passive) {
pinMode(meterConfig.txPin, OUTPUT);
digitalWrite(meterConfig.txPin, LOW);
}
hanSerial->setTimeout(250);
// Empty buffer before starting
while (hanSerial->available() > 0) {
hanSerial->read();
}
#if defined(ESP8266)
if(hwSerial != NULL) {
hwSerial->hasOverrun();
} else if(swSerial != NULL) {
swSerial->overflow();
}
#endif
}
HardwareSerial* PassiveMeterCommunicator::getHwSerial() {
return hwSerial;
}
void PassiveMeterCommunicator::rxerr(int err) {
if(err == 0) return;
if(lastError == 90+err) return; // Do not flood with same error
switch(err) {
case 2:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("Serial buffer overflow\n"));
rxBufferErrors++;
#if defined(ESP32)
if(rxBufferErrors > 1 && meterConfig.bufferSize < 8) {
meterConfig.bufferSize += 2;
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Increasing RX buffer to %d bytes\n"), meterConfig.bufferSize * 64);
configChanged = true;
rxBufferErrors = 0;
}
#endif
break;
case 3:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("Serial FIFO overflow\n"));
break;
case 4:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Serial frame error\n"));
break;
case 5:
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING))
#endif
debugger->printf_P(PSTR("Serial parity error\n"));
unsigned long now = millis();
if(autodetect) {
meterAutodetectLastChange = 0;
} else if(validDataReceived > 2) {
meterConfig.parity = getNextParity(meterConfig.parity);
configChanged = true;
}
break;
}
// Do not include serial break
if(err > 1) lastError = 90+err;
}
void PassiveMeterCommunicator::handleAutodetect(unsigned long now) {
if(!autodetect) return;
if(now - meterAutodetectLastChange < 12000) return;
if(validDataReceived < 2) {
if(meterConfig.baud == 0 || meterConfig.parity == 0) {
autodetect = true;
if(lastError == 95) { // If parity error, switch parity
autodetectParity = getNextParity(autodetectParity);
lastError = 0;
} else {
autodetectCount++;
}
if(autodetectCount == sizeof(AUTO_BAUD_RATES)/sizeof(AUTO_BAUD_RATES[0])) {
autodetectInvert = !autodetectInvert;
autodetectCount = 0;
}
autodetectBaud = AUTO_BAUD_RATES[autodetectCount];
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Meter serial autodetect, swapping to: %d, %d, %s\n"), autodetectBaud, autodetectParity, autodetectInvert ? "true" : "false");
meterConfig.bufferSize = max((uint32_t) 1, autodetectBaud / 14400);
setupHanPort(autodetectBaud, autodetectParity, autodetectInvert);
meterAutodetectLastChange = now;
}
} else if(autodetect) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Meter serial autodetected, saving: %d, %d, %s\n"), autodetectBaud, autodetectParity, autodetectInvert ? "true" : "false");
autodetect = false;
meterConfig.baud = autodetectBaud;
meterConfig.parity = autodetectParity;
meterConfig.invert = autodetectInvert;
configChanged = true;
setupHanPort(meterConfig.baud, meterConfig.parity, meterConfig.invert);
}
}
uint8_t PassiveMeterCommunicator::getNextParity(uint8_t parityOrdinal) {
switch(parityOrdinal) {
case 10: // 7E1
return 2; // 7N1
case 14: // 7E2
return 6; // 7N2
case 11: // 8E1
return 3; // 8N1
case 15: // 8E2
return 7; // 8N2
}
return 3;
}

View File

@@ -1,7 +1,17 @@
/**
* @copyright Utilitech AS 2023
* License: Fair Source
*
*/
#include "PulseMeterCommunicator.h"
#include "Uptime.h"
#if defined(AMS_REMOTE_DEBUG)
PulseMeterCommunicator::PulseMeterCommunicator(RemoteDebug* debugger) {
#else
PulseMeterCommunicator::PulseMeterCommunicator(Stream* debugger) {
#endif
this->debugger = debugger;
}
@@ -37,12 +47,19 @@ bool PulseMeterCommunicator::isConfigChanged() {
return this->configChanged;
}
void PulseMeterCommunicator::ackConfigChanged() {
configChanged = false;
}
void PulseMeterCommunicator::getCurrentConfig(MeterConfig& meterConfig) {
meterConfig = this->meterConfig;
}
void PulseMeterCommunicator::setupGpio() {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("Setting up Pulse Meter GPIO, rx: %d, tx: %d\n"), meterConfig.rxPin, meterConfig.txPin);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("Setting up Pulse Meter GPIO, rx: %d, tx: %d\n"), meterConfig.rxPin, meterConfig.txPin);
if(meterConfig.rxPin != NOT_A_PIN) {
pinMode(meterConfig.rxPin, meterConfig.rxPinPullup ? INPUT_PULLUP : INPUT);
}

View File

@@ -11,9 +11,15 @@
class PassthroughMqttHandler : public AmsMqttHandler {
public:
PassthroughMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf) : AmsMqttHandler(mqttConfig, debugger, buf) {
#if defined(AMS_REMOTE_DEBUG)
PassthroughMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
this->topic = String(mqttConfig.publishTopic);
};
#else
PassthroughMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
this->topic = String(mqttConfig.publishTopic);
};
#endif
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*);

Some files were not shown because too many files have changed in this diff Show More