Compare commits

...

132 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
ba747250b3 fix: capitalize voltage unit 'v' to 'V' in VoltPlot y-axis label
Agent-Logs-Url: https://github.com/UtilitechAS/amsreader-firmware/sessions/742a66fe-8e10-467b-97c0-f54620131698

Co-authored-by: gskjold <4446828+gskjold@users.noreply.github.com>
2026-04-15 08:15:17 +00:00
copilot-swe-agent[bot]
6d65766908 Initial plan 2026-04-15 08:14:18 +00:00
Gunnar Skjold
0bb434f1f7 Improved initial setup (#1148)
* Improved initial setup

* Improvements after testing

* Adjustments after testing

* Fixed ESP8266 build

* Fixed voltage check
2026-04-09 14:37:34 +02:00
Copilot
4673feaaf3 Fix day dropdowns in price config to respect selected month (#1168)
* Initial plan

* Fix month-dependent day dropdowns in PriceConfig.svelte

Co-authored-by: gskjold <4446828+gskjold@users.noreply.github.com>
Agent-Logs-Url: https://github.com/UtilitechAS/amsreader-firmware/sessions/cc7b8eba-e39b-461a-bd3b-7a560279afcc

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: gskjold <4446828+gskjold@users.noreply.github.com>
Co-authored-by: Gunnar Skjold <gunnar.skjold@gmail.com>
2026-04-09 12:10:28 +02:00
Gunnar Skjold
2c96b4d94f Fixed tariff peaks on wrong date and time (#1159)
* Trying to fix tariff on wrong date. Also some code cleanup

* Fix issue for ex DLMS where accumulated is always included

* Stricter time restrictions when updating history

* Adjustments after testing
2026-04-09 11:41:58 +02:00
Mads Fox
6011d3169e Fix uninitialized loop variable in GcmParser causing undefined behavior (#1163)
In GcmParser::parse(), the authentication check loop used an uninitialized
loop counter: `for(uint8_t i; i < 16; i++)`. This is undefined behavior in
C++ because `i` has an indeterminate value, potentially causing the
authentication check to be skipped entirely or to read out-of-bounds memory.

Fix: initialize `i` to 0 so the loop correctly iterates all 16 bytes of
the authentication key.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 09:51:27 +02:00
Gunnar Skjold
f0c3873635 Fixed price from config file reboot loop (#1172) 2026-04-09 09:48:02 +02:00
Gunnar Skjold
fb4eea8208 Prevent boot loop in voltage check (#1171)
* Prevent boot loop if voltage is outside operating range

* Fixed code error
2026-04-09 09:47:41 +02:00
Gunnar Skjold
e628056e56 Fixed parsing of Iskra (#1158) 2026-04-09 09:46:44 +02:00
Gunnar Skjold
df5611844f Moved reset of reboot reason to main program (#1153)
* Moved reset of reboot reason to main program

* Allow up to 8 cycles to charge capacitor
2026-04-09 09:46:23 +02:00
Gunnar Skjold
3cb6e09341 Use unique SSID on first boot (#1143)
* Use unique SSID on first boot

* Debugger adjustments
2026-04-09 09:41:16 +02:00
Gunnar Skjold
f7ccd2a96b Updated copyright (#1173) 2026-04-09 09:40:09 +02:00
Gunnar Skjold
13aff62aff Fixed PR workflow double zipping (#1162)
* Added .zip extension to avoid double zipping

* Fixed double zip
2026-03-15 10:07:45 +01:00
Gunnar Skjold
64a0667947 Added workflow to attach firmware in PR (#1160)
* Added PR workflow that creates a comment with firmware zip

* Fixed URL

* Show version in comment
2026-03-15 09:31:59 +01:00
Gunnar Skjold
009c4686ee Fixed incorrect color LED on boot (#1149) 2026-03-05 14:54:51 +01:00
dependabot[bot]
33dc5fc177 Bump rollup from 3.29.5 to 3.30.0 in /lib/SvelteUi/app (#1147)
Bumps [rollup](https://github.com/rollup/rollup) from 3.29.5 to 3.30.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/v3.30.0/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v3.29.5...v3.30.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-05 14:54:01 +01:00
dependabot[bot]
faf047e25f Bump minimatch in /lib/SvelteUi/app (#1154)
Bumps  and [minimatch](https://github.com/isaacs/minimatch). These dependencies needed to be updated together.

Updates `minimatch` from 9.0.5 to 9.0.9
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9)

Updates `minimatch` from 3.1.2 to 3.1.5
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 9.0.9
  dependency-type: indirect
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-05 14:53:36 +01:00
dependabot[bot]
b4322c5f9c Bump svgo from 2.8.0 to 2.8.2 in /lib/SvelteUi/app (#1157)
Bumps [svgo](https://github.com/svg/svgo) from 2.8.0 to 2.8.2.
- [Release notes](https://github.com/svg/svgo/releases)
- [Commits](https://github.com/svg/svgo/compare/v2.8.0...v2.8.2)

---
updated-dependencies:
- dependency-name: svgo
  dependency-version: 2.8.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-05 14:53:05 +01:00
Gunnar Skjold
0b4884652f Allow for more errors during upgrade (#1139)
* Allow for more errors during upgrade

* More instead of equals
2026-02-12 12:28:31 +01:00
Gunnar Skjold
82aeae8699 Fixed compile error for 8266 after #1121 (#1138) 2026-02-12 09:45:04 +01:00
Gunnar Skjold
a7333653b0 Fixed decimal accuracy on saved values (#1133) 2026-02-12 08:25:32 +01:00
Gunnar Skjold
24e63d5e32 Fixed HA object id (#1132) 2026-02-12 08:25:17 +01:00
Gunnar Skjold
eb7c266378 Fixed double slash on Wiki links (#1123) 2026-02-12 08:25:04 +01:00
Gunnar Skjold
cf8c48ab99 Added code to ensure stable boot (#1121)
* If BUS powered, wait for capacitor to charge on boot, this ensures better boot stability

* Some cleanup
2026-02-12 08:24:50 +01:00
Gunnar Skjold
78a1cd78ea Added support for a new format for a Iskra meter in Switzerland (#1118) 2026-02-12 08:24:26 +01:00
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
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
saddfox
165a385844 add devcontainer 2024-11-10 18:09:18 +01:00
166 changed files with 8701 additions and 6341 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: | run: |
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini 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_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_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 sed -i 's/NO_ENERGY_SPEEDOMETER_PASS/ENERGY_SPEEDOMETER_PASS=\\"${{secrets.ENERGY_SPEEDOMETER_PASS}}\\"/g' platformio.ini
- name: Cache Python dependencies - name: Cache Python dependencies
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('platformio.ini') }} key: ${{ runner.os }}-pip-${{ hashFiles('platformio.ini') }}
- name: Cache PlatformIO dependencies - name: Cache PlatformIO dependencies
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.pio/libdeps path: ~/.pio/libdeps
key: ${{ runner.os }}-pio-${{ hashFiles('platformio.ini') }} key: ${{ runner.os }}-pio-${{ hashFiles('platformio.ini') }}
@@ -50,7 +51,7 @@ jobs:
- name: Set up node - name: Set up node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '16.x' node-version: '19.x'
- name: Build with node - name: Build with node
run: | run: |
cd lib/SvelteUi/app 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/

82
.github/workflows/pr-build-env.yml vendored Normal file
View File

@@ -0,0 +1,82 @@
name: PR build with env
on:
workflow_call:
inputs:
env:
description: 'The environment to build for'
required: true
type: string
is_esp32:
description: 'Whether the build is for ESP32 based firmware'
required: false
type: boolean
default: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out code from repo
uses: actions/checkout@v4
- 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
run: pio run -e ${{ inputs.env }}
- name: Create zip file
run: /bin/sh scripts/${{ inputs.env }}/mkzip.sh
- name: Upload zip as artifact
uses: actions/upload-artifact@v7
with:
name: ${{ inputs.env }}.zip
path: ${{ inputs.env }}.zip
archive: false
retention-days: 7

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

110
.github/workflows/pull-request.yml vendored Normal file
View File

@@ -0,0 +1,110 @@
name: Pull Request build
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
build-esp32s2:
uses: ./.github/workflows/pr-build-env.yml
secrets: inherit
with:
env: esp32s2
is_esp32: true
build-esp32s3:
uses: ./.github/workflows/pr-build-env.yml
secrets: inherit
with:
env: esp32s3
is_esp32: true
build-esp32c3:
uses: ./.github/workflows/pr-build-env.yml
secrets: inherit
with:
env: esp32c3
is_esp32: true
build-esp32:
uses: ./.github/workflows/pr-build-env.yml
secrets: inherit
with:
env: esp32
is_esp32: true
build-esp32solo:
uses: ./.github/workflows/pr-build-env.yml
secrets: inherit
with:
env: esp32solo
is_esp32: true
build-esp8266:
uses: ./.github/workflows/pr-build-env.yml
secrets: inherit
with:
env: esp8266
is_esp32: false
comment:
needs:
- build-esp32s2
- build-esp32s3
- build-esp32c3
- build-esp32
- build-esp32solo
- build-esp8266
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Post PR comment with download links
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request.number;
const runId = context.runId;
const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`;
// Get the commit SHA (short version)
const sha = context.payload.pull_request.head.sha;
const shortSha = sha.substring(0, 7);
// Fetch the list of artifacts for this run via the API
const artifactsResp = await github.rest.actions.listWorkflowRunArtifacts({ owner, repo, run_id: runId });
const artifacts = artifactsResp.data.artifacts;
const envs = ['esp32s2', 'esp32s3', 'esp32c3', 'esp32', 'esp32solo', 'esp8266'];
const lines = envs.map(env => {
const artifact = artifacts.find(a => a.name === `${env}.zip`);
if (artifact) {
// The artifact download page URL - directly navigable in the browser
const artifactUrl = `${runUrl}#artifacts-${env}`;
return `- **${env}**: [Download ${env}.zip](https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${artifact.id})`;
}
return `- **${env}**: ⚠️ artifact not found`;
});
const body = [
'## 🔧 PR Build Artifacts',
'',
`**Version**: \`${shortSha}\``,
'',
'All environments built successfully. Download the zip files:',
'',
...lines,
'',
`> Artifacts expire after 7 days. [View workflow run](${runUrl})`,
].join('\n');
// Find and delete any previous bot comment to keep the PR clean
const comments = await github.rest.issues.listComments({ owner, repo, issue_number: prNumber });
for (const comment of comments.data) {
if (comment.user.type === 'Bot' && comment.body.includes('PR Build Artifacts')) {
await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id });
}
}
await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });

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

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

2
.gitignore vendored
View File

@@ -19,3 +19,5 @@ platformio-user.ini
node_modules node_modules
/gui/dist /gui/dist
/scripts/*dev /scripts/*dev
localazy-keys.json
localazy/language

View File

@@ -1,12 +1,15 @@
# AMS Reader # 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. 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"> <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 ## Building this project with PlatformIO
To build this project, you need [PlatformIO](https://platformio.org/) installed. To build this project, you need [PlatformIO](https://platformio.org/) installed.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@

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 ...

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

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -13,7 +13,6 @@
#define EEPROM_CHECK_SUM 104 // Used to check if config is stored. Change if structure changes #define EEPROM_CHECK_SUM 104 // Used to check if config is stored. Change if structure changes
#define EEPROM_CLEARED_INDICATOR 0xFC #define EEPROM_CLEARED_INDICATOR 0xFC
#define EEPROM_CONFIG_ADDRESS 0 #define EEPROM_CONFIG_ADDRESS 0
#define EEPROM_TEMP_CONFIG_ADDRESS 2048
#define CONFIG_SYSTEM_START 8 #define CONFIG_SYSTEM_START 8
#define CONFIG_NETWORK_START 40 #define CONFIG_NETWORK_START 40
@@ -30,6 +29,7 @@
#define CONFIG_UI_START 1720 #define CONFIG_UI_START 1720
#define CONFIG_CLOUD_START 1742 #define CONFIG_CLOUD_START 1742
#define CONFIG_UPGRADE_INFO_START 1934 #define CONFIG_UPGRADE_INFO_START 1934
#define CONFIG_ZC_START 2000
#define CONFIG_METER_START_103 32 #define CONFIG_METER_START_103 32
#define CONFIG_UPGRADE_INFO_START_103 216 #define CONFIG_UPGRADE_INFO_START_103 216
@@ -50,6 +50,22 @@
#define LED_BEHAVIOUR_ERROR_ONLY 3 #define LED_BEHAVIOUR_ERROR_ONLY 3
#define LED_BEHAVIOUR_OFF 9 #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 { struct ResetDataContainer {
uint8_t cause; uint8_t cause;
uint8_t last_cause; uint8_t last_cause;
@@ -63,7 +79,8 @@ struct SystemConfig {
uint8_t dataCollectionConsent; // 0 = unknown, 1 = accepted, 2 = declined uint8_t dataCollectionConsent; // 0 = unknown, 1 = accepted, 2 = declined
char country[3]; char country[3];
uint8_t energyspeedometer; uint8_t energyspeedometer;
}; // 8 uint8_t firmwareChannel;
}; // 9
struct NetworkConfig { struct NetworkConfig {
char ssid[32]; char ssid[32];
@@ -97,7 +114,8 @@ struct MqttConfig {
uint16_t stateUpdateInterval; uint16_t stateUpdateInterval;
uint16_t timeout; uint16_t timeout;
uint8_t keepalive; uint8_t keepalive;
}; // 685 uint8_t rebootMinutes;
}; // 684
struct WebConfig { struct WebConfig {
uint8_t security; uint8_t security;
@@ -157,7 +175,8 @@ struct GpioConfig {
uint16_t vccResistorVcc; uint16_t vccResistorVcc;
uint8_t ledDisablePin; uint8_t ledDisablePin;
uint8_t ledBehaviour; uint8_t ledBehaviour;
}; // 21 uint8_t powersaving;
}; // 22
struct GpioConfig103 { struct GpioConfig103 {
uint8_t hanPin; uint8_t hanPin;
@@ -206,10 +225,12 @@ struct PriceServiceConfig {
char entsoeToken[37]; char entsoeToken[37];
char area[17]; char area[17];
char currency[4]; char currency[4];
uint32_t unused1; uint8_t resolutionInMinutes;
bool enabled;
uint16_t unused2; uint16_t unused2;
}; // 64 uint16_t unused3;
bool enabled;
uint16_t unused6;
};
struct EnergyAccountingConfig { struct EnergyAccountingConfig {
uint16_t thresholds[10]; uint16_t thresholds[10];
@@ -236,14 +257,14 @@ struct UiConfig {
}; // 15 }; // 15
struct UpgradeInformation { struct UpgradeInformation {
char fromVersion[8]; char fromVersion[16];
char toVersion[8]; char toVersion[16];
uint32_t size; uint32_t size;
uint16_t block_position; uint16_t block_position;
uint8_t retry_count; uint8_t retry_count;
uint8_t reboot_count; uint8_t reboot_count;
int8_t errorCode; int8_t errorCode;
}; // 25 }; // 41+3
struct CloudConfig { struct CloudConfig {
bool enabled; bool enabled;
@@ -254,6 +275,12 @@ struct CloudConfig {
uint8_t proto; uint8_t proto;
}; // 88 }; // 88
struct ZmartChargeConfig {
bool enabled;
char token[21];
char baseUrl[64];
}; // 86
class AmsConfiguration { class AmsConfiguration {
public: public:
bool hasConfig(); bool hasConfig();
@@ -283,6 +310,8 @@ public:
bool getWebConfig(WebConfig&); bool getWebConfig(WebConfig&);
bool setWebConfig(WebConfig&); bool setWebConfig(WebConfig&);
void clearWebConfig(WebConfig&); void clearWebConfig(WebConfig&);
bool isWebChanged();
void ackWebChange();
bool getMeterConfig(MeterConfig&); bool getMeterConfig(MeterConfig&);
bool setMeterConfig(MeterConfig&); bool setMeterConfig(MeterConfig&);
@@ -346,6 +375,15 @@ public:
bool isCloudChanged(); bool isCloudChanged();
void ackCloudConfig(); void ackCloudConfig();
bool getZmartChargeConfig(ZmartChargeConfig&);
bool setZmartChargeConfig(ZmartChargeConfig&);
void clearZmartChargeConfig(ZmartChargeConfig&);
bool isZmartChargeConfigChanged();
void ackZmartChargeConfig();
uint32_t getChipId();
void getUniqueName(char* buffer, size_t length);
void clear(); void clear();
protected: protected:
@@ -353,7 +391,7 @@ protected:
private: private:
uint8_t configVersion = 0; 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 relocateConfig103(); // 2.2.12, until, but not including 2.3 bool relocateConfig103(); // 2.2.12, until, but not including 2.3

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -13,7 +13,7 @@
String toHex(uint8_t* in); String toHex(uint8_t* in);
String toHex(uint8_t* in, uint16_t size); String toHex(uint8_t* in, uint16_t size);
void fromHex(uint8_t *out, String 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); void debugPrint(uint8_t *buffer, uint16_t start, uint16_t length, Print* debugger);
#endif #endif

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -13,15 +13,26 @@
bool AmsConfiguration::getSystemConfig(SystemConfig& config) { bool AmsConfiguration::getSystemConfig(SystemConfig& config) {
EEPROM.begin(EEPROM_SIZE); EEPROM.begin(EEPROM_SIZE);
uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS); uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
if(configVersion == EEPROM_CHECK_SUM || configVersion == EEPROM_CLEARED_INDICATOR) { EEPROM.get(CONFIG_SYSTEM_START, config);
EEPROM.get(CONFIG_SYSTEM_START, config); EEPROM.end();
EEPROM.end();
if(config.firmwareChannel > 3) {
config.firmwareChannel = 0;
}
if(configVersion == EEPROM_CHECK_SUM) {
return true; return true;
} else { } else {
config.boardType = 0xFF; if(configVersion == EEPROM_CLEARED_INDICATOR && config.boardType > 0 && config.boardType < 250) {
config.vendorConfigured = false; config.vendorConfigured = true;
} else {
config.vendorConfigured = false;
config.boardType = 0xFF;
clear();
}
config.userConfigured = false; config.userConfigured = false;
config.dataCollectionConsent = 0; config.dataCollectionConsent = 0;
config.firmwareChannel = 0;
config.energyspeedometer = 0; config.energyspeedometer = 0;
memset(config.country, 0, 3); memset(config.country, 0, 3);
return false; return false;
@@ -37,6 +48,9 @@ bool AmsConfiguration::setSystemConfig(SystemConfig& config) {
sysChanged |= config.dataCollectionConsent != existing.dataCollectionConsent; sysChanged |= config.dataCollectionConsent != existing.dataCollectionConsent;
sysChanged |= strcmp(config.country, existing.country) != 0; sysChanged |= strcmp(config.country, existing.country) != 0;
sysChanged |= config.energyspeedometer != existing.energyspeedometer; sysChanged |= config.energyspeedometer != existing.energyspeedometer;
sysChanged |= config.firmwareChannel != existing.firmwareChannel;
} else {
sysChanged = true;
} }
EEPROM.begin(EEPROM_SIZE); EEPROM.begin(EEPROM_SIZE);
stripNonAscii((uint8_t*) config.country, 2); 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.ssid, 32, true);
stripNonAscii((uint8_t*) config.psk, 64, true); stripNonAscii((uint8_t*) config.psk, 64, true, false);
stripNonAscii((uint8_t*) config.ip, 16); stripNonAscii((uint8_t*) config.ip, 16);
stripNonAscii((uint8_t*) config.gateway, 16); stripNonAscii((uint8_t*) config.gateway, 16);
stripNonAscii((uint8_t*) config.subnet, 16); stripNonAscii((uint8_t*) config.subnet, 16);
@@ -110,16 +124,12 @@ void AmsConfiguration::clearNetworkConfig(NetworkConfig& config) {
memset(config.ssid, 0, 32); memset(config.ssid, 0, 32);
memset(config.psk, 0, 64); memset(config.psk, 0, 64);
clearNetworkConfigIp(config); clearNetworkConfigIp(config);
getUniqueName(config.hostname, 32);
uint16_t chipId;
#if defined(ESP32) #if defined(ESP32)
chipId = ( ESP.getEfuseMac() >> 32 ) % 0xFFFFFFFF;
config.power = 195; config.power = 195;
#else #else
chipId = ESP.getChipId();
config.power = 205; config.power = 205;
#endif #endif
strcpy(config.hostname, (String("ams-") + String(chipId, HEX)).c_str());
config.mdns = true; config.mdns = true;
config.sleep = 0xFF; config.sleep = 0xFF;
config.use11b = 1; config.use11b = 1;
@@ -147,14 +157,17 @@ bool AmsConfiguration::getMqttConfig(MqttConfig& config) {
EEPROM.begin(EEPROM_SIZE); EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_MQTT_START, config); EEPROM.get(CONFIG_MQTT_START, config);
EEPROM.end(); EEPROM.end();
if(config.magic != 0x9C) { if(config.magic != 0xA5) { // New magic for 2.4.11
if(config.magic != 0x7B) { if(config.magic != 0x9C) {
config.stateUpdate = false; if(config.magic != 0x7B) {
config.stateUpdateInterval = 10; config.stateUpdate = false;
config.stateUpdateInterval = 10;
}
config.timeout = 1000;
config.keepalive = 60;
} }
config.timeout = 1000; config.rebootMinutes = config.ssl ? 5 : 0;
config.keepalive = 60; config.magic = 0xA5;
config.magic = 0x9C;
} }
return true; return true;
} else { } else {
@@ -177,6 +190,9 @@ bool AmsConfiguration::setMqttConfig(MqttConfig& config) {
mqttChanged |= config.ssl != existing.ssl; mqttChanged |= config.ssl != existing.ssl;
mqttChanged |= config.stateUpdate != existing.stateUpdate; mqttChanged |= config.stateUpdate != existing.stateUpdate;
mqttChanged |= config.stateUpdateInterval != existing.stateUpdateInterval; mqttChanged |= config.stateUpdateInterval != existing.stateUpdateInterval;
mqttChanged |= config.timeout != existing.timeout;
mqttChanged |= config.keepalive != existing.keepalive;
mqttChanged |= config.rebootMinutes != existing.rebootMinutes;
} else { } else {
mqttChanged = true; mqttChanged = true;
} }
@@ -186,11 +202,12 @@ bool AmsConfiguration::setMqttConfig(MqttConfig& config) {
stripNonAscii((uint8_t*) config.publishTopic, 64); stripNonAscii((uint8_t*) config.publishTopic, 64);
stripNonAscii((uint8_t*) config.subscribeTopic, 64); stripNonAscii((uint8_t*) config.subscribeTopic, 64);
stripNonAscii((uint8_t*) config.username, 128, true); stripNonAscii((uint8_t*) config.username, 128, true);
stripNonAscii((uint8_t*) config.password, 256, true); stripNonAscii((uint8_t*) config.password, 256, true, false);
if(config.timeout < 500) config.timeout = 1000; if(config.timeout < 500) config.timeout = 1000;
if(config.timeout > 10000) config.timeout = 1000; if(config.timeout > 10000) config.timeout = 1000;
if(config.keepalive < 5) config.keepalive = 60; if(config.keepalive < 5) config.keepalive = 60;
if(config.keepalive > 240) config.keepalive = 60; if(config.keepalive > 240) config.keepalive = 60;
if(config.rebootMinutes > 240) config.rebootMinutes = 0;
EEPROM.begin(EEPROM_SIZE); EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_MQTT_START, config); EEPROM.put(CONFIG_MQTT_START, config);
@@ -215,6 +232,7 @@ void AmsConfiguration::clearMqtt(MqttConfig& config) {
config.stateUpdateInterval = 10; config.stateUpdateInterval = 10;
config.timeout = 1000; config.timeout = 1000;
config.keepalive = 60; config.keepalive = 60;
config.rebootMinutes = 0;
} }
void AmsConfiguration::setMqttChanged() { void AmsConfiguration::setMqttChanged() {
@@ -242,9 +260,17 @@ bool AmsConfiguration::getWebConfig(WebConfig& config) {
} }
bool AmsConfiguration::setWebConfig(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.username, 37);
stripNonAscii((uint8_t*) config.password, 37); stripNonAscii((uint8_t*) config.password, 37, false, false);
stripNonAscii((uint8_t*) config.context, 37); stripNonAscii((uint8_t*) config.context, 37);
EEPROM.begin(EEPROM_SIZE); EEPROM.begin(EEPROM_SIZE);
@@ -261,9 +287,18 @@ void AmsConfiguration::clearWebConfig(WebConfig& config) {
memset(config.context, 0, 37); memset(config.context, 0, 37);
} }
bool AmsConfiguration::isWebChanged() {
return webChanged;
}
void AmsConfiguration::ackWebChange() {
webChanged = false;
}
bool AmsConfiguration::getMeterConfig(MeterConfig& config) { 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.get(CONFIG_METER_START, config);
EEPROM.end(); EEPROM.end();
if(config.bufferSize < 1 || config.bufferSize > 64) { if(config.bufferSize < 1 || config.bufferSize > 64) {
@@ -478,6 +513,7 @@ bool AmsConfiguration::getGpioConfig(GpioConfig& config) {
if(configVersion == EEPROM_CHECK_SUM || configVersion == EEPROM_CLEARED_INDICATOR) { if(configVersion == EEPROM_CHECK_SUM || configVersion == EEPROM_CLEARED_INDICATOR) {
EEPROM.get(CONFIG_GPIO_START, config); EEPROM.get(CONFIG_GPIO_START, config);
EEPROM.end(); EEPROM.end();
if(config.powersaving > 4) config.powersaving = 0;
return true; return true;
} else { } else {
clearGpio(config); clearGpio(config);
@@ -559,6 +595,7 @@ void AmsConfiguration::clearGpio(GpioConfig& config, bool all) {
config.vccResistorGnd = 0; config.vccResistorGnd = 0;
config.vccResistorVcc = 0; config.vccResistorVcc = 0;
config.ledBehaviour = LED_BEHAVIOUR_DEFAULT; config.ledBehaviour = LED_BEHAVIOUR_DEFAULT;
config.powersaving = 0;
} }
} }
@@ -625,6 +662,9 @@ bool AmsConfiguration::getPriceServiceConfig(PriceServiceConfig& config) {
clearPriceServiceConfig(config); clearPriceServiceConfig(config);
return false; return false;
} }
if(config.resolutionInMinutes != 15 && config.resolutionInMinutes != 60) {
config.resolutionInMinutes = 60;
}
return true; return true;
} else { } else {
clearPriceServiceConfig(config); clearPriceServiceConfig(config);
@@ -639,6 +679,7 @@ bool AmsConfiguration::setPriceServiceConfig(PriceServiceConfig& config) {
priceChanged |= strcmp(config.area, existing.area) != 0; priceChanged |= strcmp(config.area, existing.area) != 0;
priceChanged |= strcmp(config.currency, existing.currency) != 0; priceChanged |= strcmp(config.currency, existing.currency) != 0;
priceChanged |= config.enabled != existing.enabled; priceChanged |= config.enabled != existing.enabled;
priceChanged |= config.resolutionInMinutes != existing.resolutionInMinutes;
} else { } else {
priceChanged = true; priceChanged = true;
} }
@@ -658,9 +699,8 @@ void AmsConfiguration::clearPriceServiceConfig(PriceServiceConfig& config) {
memset(config.entsoeToken, 0, 37); memset(config.entsoeToken, 0, 37);
memset(config.area, 0, 17); memset(config.area, 0, 17);
memset(config.currency, 0, 4); memset(config.currency, 0, 4);
config.unused1 = 1000;
config.enabled = false; config.enabled = false;
config.unused2 = 0; config.resolutionInMinutes = 60;
} }
bool AmsConfiguration::isPriceServiceChanged() { bool AmsConfiguration::isPriceServiceChanged() {
@@ -788,8 +828,8 @@ void AmsConfiguration::ackUiLanguageChange() {
} }
bool AmsConfiguration::setUpgradeInformation(UpgradeInformation& upinfo) { bool AmsConfiguration::setUpgradeInformation(UpgradeInformation& upinfo) {
stripNonAscii((uint8_t*) upinfo.fromVersion, 8); stripNonAscii((uint8_t*) upinfo.fromVersion, 16);
stripNonAscii((uint8_t*) upinfo.toVersion, 8); stripNonAscii((uint8_t*) upinfo.toVersion, 16);
EEPROM.begin(EEPROM_SIZE); EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_UPGRADE_INFO_START, upinfo); EEPROM.put(CONFIG_UPGRADE_INFO_START, upinfo);
@@ -803,7 +843,7 @@ bool AmsConfiguration::getUpgradeInformation(UpgradeInformation& upinfo) {
EEPROM.begin(EEPROM_SIZE); EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_UPGRADE_INFO_START, upinfo); EEPROM.get(CONFIG_UPGRADE_INFO_START, upinfo);
EEPROM.end(); 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); clearUpgradeInformation(upinfo);
return false; return false;
} }
@@ -815,8 +855,8 @@ bool AmsConfiguration::getUpgradeInformation(UpgradeInformation& upinfo) {
} }
void AmsConfiguration::clearUpgradeInformation(UpgradeInformation& upinfo) { void AmsConfiguration::clearUpgradeInformation(UpgradeInformation& upinfo) {
memset(upinfo.fromVersion, 0, 8); memset(upinfo.fromVersion, 0, 16);
memset(upinfo.toVersion, 0, 8); memset(upinfo.toVersion, 0, 16);
upinfo.errorCode = 0; upinfo.errorCode = 0;
upinfo.size = 0; upinfo.size = 0;
upinfo.block_position = 0; upinfo.block_position = 0;
@@ -876,10 +916,86 @@ void AmsConfiguration::ackCloudConfig() {
cloudChanged = false; 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() { void AmsConfiguration::setUiLanguageChanged() {
uiLanguageChanged = true; uiLanguageChanged = true;
} }
uint32_t AmsConfiguration::getChipId() {
uint32_t chipId;
#if defined(ESP32)
for(int i=0; i<17; i=i+8) {
chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
}
#else
chipId = ESP.getChipId();
#endif
return chipId;
}
void AmsConfiguration::getUniqueName(char* buffer, size_t length) {
uint32_t chipId = getChipId();
snprintf(buffer, length, "ams-%06x", chipId);
}
void AmsConfiguration::clear() { void AmsConfiguration::clear() {
EEPROM.begin(EEPROM_SIZE); EEPROM.begin(EEPROM_SIZE);
@@ -887,6 +1003,7 @@ void AmsConfiguration::clear() {
EEPROM.get(CONFIG_SYSTEM_START, sys); EEPROM.get(CONFIG_SYSTEM_START, sys);
sys.userConfigured = false; sys.userConfigured = false;
sys.dataCollectionConsent = 0; sys.dataCollectionConsent = 0;
sys.firmwareChannel = 0;
sys.energyspeedometer = 0; sys.energyspeedometer = 0;
memset(sys.country, 0, 3); memset(sys.country, 0, 3);
EEPROM.put(CONFIG_SYSTEM_START, sys); EEPROM.put(CONFIG_SYSTEM_START, sys);
@@ -943,6 +1060,10 @@ void AmsConfiguration::clear() {
clearCloudConfig(cloud); clearCloudConfig(cloud);
EEPROM.put(CONFIG_CLOUD_START, 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.put(EEPROM_CONFIG_ADDRESS, EEPROM_CLEARED_INDICATOR);
EEPROM.commit(); EEPROM.commit();
EEPROM.end(); EEPROM.end();
@@ -1045,7 +1166,8 @@ bool AmsConfiguration::relocateConfig103() {
gpio103.vccResistorGnd, gpio103.vccResistorGnd,
gpio103.vccResistorVcc, gpio103.vccResistorVcc,
gpio103.ledDisablePin, gpio103.ledDisablePin,
gpio103.ledBehaviour gpio103.ledBehaviour,
0
}; };
WebConfig web = {web103.security}; WebConfig web = {web103.security};
@@ -1077,6 +1199,10 @@ bool AmsConfiguration::relocateConfig103() {
clearCloudConfig(cloud); clearCloudConfig(cloud);
EEPROM.put(CONFIG_CLOUD_START, cloud); EEPROM.put(CONFIG_CLOUD_START, cloud);
ZmartChargeConfig zcc;
clearZmartChargeConfig(zcc);
EEPROM.put(CONFIG_ZC_START, zcc);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 104); EEPROM.put(EEPROM_CONFIG_ADDRESS, 104);
bool ret = EEPROM.commit(); bool ret = EEPROM.commit();
EEPROM.end(); EEPROM.end();
@@ -1085,6 +1211,7 @@ bool AmsConfiguration::relocateConfig103() {
bool AmsConfiguration::save() { bool AmsConfiguration::save() {
EEPROM.begin(EEPROM_SIZE); EEPROM.begin(EEPROM_SIZE);
uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM); EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM);
bool success = EEPROM.commit(); bool success = EEPROM.commit();
EEPROM.end(); EEPROM.end();
@@ -1156,6 +1283,9 @@ void AmsConfiguration::print(Print* debugger)
} }
debugger->printf_P(PSTR("Payload format: %i\r\n"), mqtt.payloadFormat); 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("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 { } else {
debugger->printf_P(PSTR("Enabled: No\r\n")); debugger->printf_P(PSTR("Enabled: No\r\n"));
} }
@@ -1209,6 +1339,7 @@ void AmsConfiguration::print(Print* debugger)
debugger->printf_P(PSTR("Vcc pin: %i\r\n"), gpio.vccPin); 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 disable pin: %i\r\n"), gpio.ledDisablePin);
debugger->printf_P(PSTR("LED behaviour: %i\r\n"), gpio.ledBehaviour); 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) { if(gpio.vccMultiplier > 0) {
debugger->printf_P(PSTR("Vcc multiplier: %f\r\n"), gpio.vccMultiplier / 1000.0); debugger->printf_P(PSTR("Vcc multiplier: %f\r\n"), gpio.vccMultiplier / 1000.0);
} }
@@ -1264,10 +1395,10 @@ void AmsConfiguration::print(Print* debugger)
debugger->printf_P(PSTR("Area: %s\r\n"), price.area); debugger->printf_P(PSTR("Area: %s\r\n"), price.area);
debugger->printf_P(PSTR("Currency: %s\r\n"), price.currency); debugger->printf_P(PSTR("Currency: %s\r\n"), price.currency);
debugger->printf_P(PSTR("ENTSO-E Token: %s\r\n"), price.entsoeToken); 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; UiConfig ui;
@@ -1285,9 +1416,29 @@ void AmsConfiguration::print(Print* debugger)
String uuid = ESPRandom::uuidToString(cc.clientId);; String uuid = ESPRandom::uuidToString(cc.clientId);;
debugger->println(F("--Cloud configuration--")); debugger->println(F("--Cloud configuration--"));
debugger->printf_P(PSTR("Enabled: %s\r\n"), cc.enabled ? "Yes" : "No"); debugger->printf_P(PSTR("Enabled: %s\r\n"), cc.enabled ? "Yes" : "No");
debugger->printf_P(PSTR("Hostname: %s\r\n"), cc.hostname); if(cc.enabled) {
debugger->printf_P(PSTR("Client ID: %s\r\n"), uuid.c_str()); debugger->printf_P(PSTR("Hostname: %s\r\n"), cc.hostname);
debugger->printf_P(PSTR("Interval: %d\r\n"), cc.interval); 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 #endif

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -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; bool ret = false;
for(uint16_t i = 0; i < size; i++) { for(uint16_t i = 0; i < size; i++) {
if(in[i] == 0) { // Clear the rest with null-terminator if(in[i] == 0) { // Clear the rest with null-terminator
memset(in+i, 0, size-i); memset(in+i, 0, size-i);
break; 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); memset(in+i, ' ', 1);
ret = true; ret = true;
} else if(!extended && (in[i] < 32 || in[i] > 126)) { } else if(!extended && (in[i] < 32 || in[i] > 126)) {
@@ -43,6 +43,22 @@ bool stripNonAscii(uint8_t* in, uint16_t size, bool extended) {
ret = true; 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 memset(in+size-1, 0, 1); // Make sure the last character is null-terminator
return ret; return ret;
} }

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -7,8 +7,7 @@
#ifndef _AMSDATA_H #ifndef _AMSDATA_H
#define _AMSDATA_H #define _AMSDATA_H
#include "Arduino.h" #include <WString.h>
#include <Timezone.h>
#include "OBIScodes.h" #include "OBIScodes.h"
enum AmsType { enum AmsType {
@@ -28,7 +27,7 @@ public:
AmsData(); AmsData();
void apply(AmsData& other); void apply(AmsData& other);
void apply(const OBIS_code_t obis, double value); void apply(const OBIS_code_t obis, double value, uint64_t millis64);
uint64_t getLastUpdateMillis(); uint64_t getLastUpdateMillis();

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,10 +1,11 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
#include "AmsData.h" #include "AmsData.h"
#include <algorithm>
AmsData::AmsData() {} AmsData::AmsData() {}
@@ -17,7 +18,6 @@ void AmsData::apply(AmsData& other) {
uint32_t power = (activeImportPower + other.getActiveImportPower()) / 2; uint32_t power = (activeImportPower + other.getActiveImportPower()) / 2;
float add = power * (((float) ms) / 3600000.0); float add = power * (((float) ms) / 3600000.0);
activeImportCounter += add / 1000.0; activeImportCounter += add / 1000.0;
//Serial.printf("%dW, %dms, %.6fkWh added\n", other.getActiveImportPower(), ms, add);
} }
if(other.getListType() > 1) { if(other.getListType() > 1) {
@@ -112,7 +112,7 @@ void AmsData::apply(AmsData& other) {
this->activeExportPower = other.getActiveExportPower(); this->activeExportPower = other.getActiveExportPower();
} }
void AmsData::apply(OBIS_code_t obis, double value) { void AmsData::apply(OBIS_code_t obis, double value, uint64_t millis64) {
if(obis.sensor == 0 && obis.gr == 0 && obis.tariff == 0) { if(obis.sensor == 0 && obis.gr == 0 && obis.tariff == 0) {
meterType = value; meterType = value;
} }
@@ -127,138 +127,137 @@ void AmsData::apply(OBIS_code_t obis, double value) {
} }
} }
if(obis.tariff != 0) { if(obis.tariff != 0) {
Serial.println("Tariff not implemented");
return; return;
} }
if(obis.gr == 7) { // Instant values if(obis.gr == 7) { // Instant values
switch(obis.sensor) { switch(obis.sensor) {
case 1: case 1:
activeImportPower = value; activeImportPower = value;
listType = max(listType, (uint8_t) 2); listType = std::max(listType, (uint8_t) 2);
break; break;
case 2: case 2:
activeExportPower = value; activeExportPower = value;
listType = max(listType, (uint8_t) 2); listType = std::max(listType, (uint8_t) 2);
break; break;
case 3: case 3:
reactiveImportPower = value; reactiveImportPower = value;
listType = max(listType, (uint8_t) 2); listType = std::max(listType, (uint8_t) 2);
break; break;
case 4: case 4:
reactiveExportPower = value; reactiveExportPower = value;
listType = max(listType, (uint8_t) 2); listType = std::max(listType, (uint8_t) 2);
break; break;
case 13: case 13:
powerFactor = value; powerFactor = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
case 21: case 21:
l1activeImportPower = value; l1activeImportPower = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
case 22: case 22:
l1activeExportPower = value; l1activeExportPower = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
case 31: case 31:
l1current = value; l1current = value;
listType = max(listType, (uint8_t) 2); listType = std::max(listType, (uint8_t) 2);
break; break;
case 32: case 32:
l1voltage = value; l1voltage = value;
listType = max(listType, (uint8_t) 2); listType = std::max(listType, (uint8_t) 2);
break; break;
case 33: case 33:
l1PowerFactor = value; l1PowerFactor = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
case 41: case 41:
l2activeImportPower = value; l2activeImportPower = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
case 42: case 42:
l2activeExportPower = value; l2activeExportPower = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
case 51: case 51:
l2current = value; l2current = value;
listType = max(listType, (uint8_t) 2); listType = std::max(listType, (uint8_t) 2);
break; break;
case 52: case 52:
l2voltage = value; l2voltage = value;
listType = max(listType, (uint8_t) 2); listType = std::max(listType, (uint8_t) 2);
break; break;
case 53: case 53:
l2PowerFactor = value; l2PowerFactor = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
case 61: case 61:
l3activeImportPower = value; l3activeImportPower = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
case 62: case 62:
l3activeExportPower = value; l3activeExportPower = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
case 71: case 71:
l3current = value; l3current = value;
listType = max(listType, (uint8_t) 2); listType = std::max(listType, (uint8_t) 2);
break; break;
case 72: case 72:
l3voltage = value; l3voltage = value;
listType = max(listType, (uint8_t) 2); listType = std::max(listType, (uint8_t) 2);
break; break;
case 73: case 73:
l3PowerFactor = value; l3PowerFactor = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
} }
} else if(obis.gr == 8) { // Accumulated values } else if(obis.gr == 8) { // Accumulated values
switch(obis.sensor) { switch(obis.sensor) {
case 1: case 1:
activeImportCounter = value; activeImportCounter = value;
listType = max(listType, (uint8_t) 3); listType = std::max(listType, (uint8_t) 3);
break; break;
case 2: case 2:
activeExportCounter = value; activeExportCounter = value;
listType = max(listType, (uint8_t) 3); listType = std::max(listType, (uint8_t) 3);
break; break;
case 3: case 3:
reactiveImportCounter = value; reactiveImportCounter = value;
listType = max(listType, (uint8_t) 3); listType = std::max(listType, (uint8_t) 3);
break; break;
case 4: case 4:
reactiveExportCounter = value; reactiveExportCounter = value;
listType = max(listType, (uint8_t) 3); listType = std::max(listType, (uint8_t) 3);
break; break;
case 21: case 21:
l1activeImportCounter = value; l1activeImportCounter = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
case 22: case 22:
l1activeExportCounter = value; l1activeExportCounter = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
case 41: case 41:
l2activeImportCounter = value; l2activeImportCounter = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
case 42: case 42:
l2activeExportCounter = value; l2activeExportCounter = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
case 61: case 61:
l3activeImportCounter = value; l3activeImportCounter = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
case 62: case 62:
l3activeExportCounter = value; l3activeExportCounter = value;
listType = max(listType, (uint8_t) 4); listType = std::max(listType, (uint8_t) 4);
break; break;
} }
} }
if(listType > 0) if(listType > 0)
lastUpdateMillis = millis(); lastUpdateMillis = millis64;
threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0; threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0;
if(!threePhase) if(!threePhase)

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -639,25 +639,22 @@ bool AmsDataStorage::isDayHappy(time_t now) {
return false; return false;
} }
if(now < FirmwareVersion::BuildEpoch) return false; // If the timestamp is before the firware was built, there is something seriously wrong..
if(now < FirmwareVersion::BuildEpoch) {
if(now < day.lastMeterReadTime) {
return false; return false;
} }
// 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 the timestamp is before the last time we updated, there is also something wrong..
if(now-day.lastMeterReadTime > 3500) { if(now < day.lastMeterReadTime) {
return false; return false;
} }
tmElements_t tm, last; tmElements_t tm, last;
breakTime(tz->toLocal(now), tm); breakTime(tz->toLocal(now), tm);
breakTime(tz->toLocal(day.lastMeterReadTime), last); breakTime(tz->toLocal(day.lastMeterReadTime), last);
if(tm.Hour != last.Hour) {
return false;
}
return true; // If the timestamp is at the same day and hour as last update, we are happy
return tm.Day == last.Day && tm.Hour == last.Hour;
} }
bool AmsDataStorage::isMonthHappy(time_t now) { bool AmsDataStorage::isMonthHappy(time_t now) {

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -24,7 +24,7 @@
#define DATA_PARSE_OK 0 #define DATA_PARSE_OK 0
#define DATA_PARSE_FAIL -1 #define DATA_PARSE_FAIL -1
#define DATA_PARSE_INCOMPLETE -2 #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_HEADER_CHECKSUM_ERROR -4
#define DATA_PARSE_FOOTER_CHECKSUM_ERROR -5 #define DATA_PARSE_FOOTER_CHECKSUM_ERROR -5
#define DATA_PARSE_INTERMEDIATE_SEGMENT -6 #define DATA_PARSE_INTERMEDIATE_SEGMENT -6

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -19,8 +19,6 @@ time_t decodeCosemDateTime(CosemDateTime timestamp) {
tm.Minute = timestamp.minute; tm.Minute = timestamp.minute;
tm.Second = timestamp.second; 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); time_t time = makeTime(tm);
int16_t deviation = ntohs(timestamp.deviation); int16_t deviation = ntohs(timestamp.deviation);
if(deviation >= -720 && deviation <= 720) { if(deviation >= -720 && deviation <= 720) {

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -17,7 +17,7 @@ int8_t DSMRParser::parse(uint8_t *buf, DataParserContext &ctx, bool verified, Pr
uint8_t lastByte = 0x00; uint8_t lastByte = 0x00;
for(uint16_t pos = 0; pos < ctx.length; pos++) { for(uint16_t pos = 0; pos < ctx.length; pos++) {
uint8_t b = *(buf+pos); uint8_t b = *(buf+pos);
if(pos == 0 && b != '/') return DATA_PARSE_BOUNDRY_FLAG_MISSING; if(pos == 0 && b != '/') return DATA_PARSE_BOUNDARY_FLAG_MISSING;
if(pos > 0 && b == '!') crcPos = pos+1; if(pos > 0 && b == '!') crcPos = pos+1;
if(crcPos > 0 && b == 0x0A && lastByte == 0x0D) { if(crcPos > 0 && b == 0x0A && lastByte == 0x0D) {
reachedEnd = true; reachedEnd = true;
@@ -74,7 +74,7 @@ int8_t DSMRParser::parse(uint8_t *buf, DataParserContext &ctx, bool verified, Pr
fromHex((uint8_t*) &crc, String((char*) buf+crcPos), 2); fromHex((uint8_t*) &crc, String((char*) buf+crcPos), 2);
crc = ntohs(crc); crc = ntohs(crc);
if(crc != crc_calc) { if(crc > 0 && crc != crc_calc) {
if(debugger != NULL) { if(debugger != NULL) {
debugger->printf_P(PSTR("CRC incorrrect, %04X != %04X at position %lu\n"), crc, crc_calc, crcPos); debugger->printf_P(PSTR("CRC incorrrect, %04X != %04X at position %lu\n"), crc, crc_calc, crcPos);
} }

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -11,7 +11,7 @@ int8_t GBTParser::parse(uint8_t *d, DataParserContext &ctx) {
GBTHeader* h = (GBTHeader*) (d); GBTHeader* h = (GBTHeader*) (d);
uint16_t sequence = ntohs(h->sequence); 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(sequence == 1) {
if(buf == NULL) buf = (uint8_t *)malloc((size_t)1024); // TODO find out from first package ? if(buf == NULL) buf = (uint8_t *)malloc((size_t)1024); // TODO find out from first package ?

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -23,7 +23,7 @@ int8_t GCMParser::parse(uint8_t *d, DataParserContext &ctx, bool hastag) {
uint32_t headersize = 0; uint32_t headersize = 0;
uint8_t* ptr = (uint8_t*) d; uint8_t* ptr = (uint8_t*) d;
if(hastag) { if(hastag) {
if(*ptr != GCM_TAG) return DATA_PARSE_BOUNDRY_FLAG_MISSING; if(*ptr != GCM_TAG) return DATA_PARSE_BOUNDARY_FLAG_MISSING;
ptr++; ptr++;
headersize++; headersize++;
} }
@@ -96,7 +96,7 @@ int8_t GCMParser::parse(uint8_t *d, DataParserContext &ctx, bool hastag) {
footersize += authkeylen; footersize += authkeylen;
memcpy(additional_authenticated_data + 1, authentication_key, 16); memcpy(additional_authenticated_data + 1, authentication_key, 16);
memcpy(authentication_tag, ptr + len - footersize - 5, authkeylen); memcpy(authentication_tag, ptr + len - footersize - 5, authkeylen);
for(uint8_t i; i < 16; i++) authenticate |= authentication_key[i] > 0; for(uint8_t i = 0; i < 16; i++) authenticate |= authentication_key[i] > 0;
} }
#if defined(ESP8266) #if defined(ESP8266)

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -29,10 +29,10 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
// First and last byte should be HDLC_FLAG // First and last byte should be HDLC_FLAG
if(h->flag != HDLC_FLAG || f->flag != 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 // 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; return DATA_PARSE_FOOTER_CHECKSUM_ERROR;
// Skip destination address, LSB marks last byte // Skip destination address, LSB marks last byte
@@ -50,7 +50,7 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
HDLC3CtrlHcs* t3 = (HDLC3CtrlHcs*) (ptr); HDLC3CtrlHcs* t3 = (HDLC3CtrlHcs*) (ptr);
// Verify HCS // 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; return DATA_PARSE_HEADER_CHECKSUM_ERROR;
ptr += 3; ptr += 3;
@@ -69,7 +69,12 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
if(buf == NULL) return DATA_PARSE_FAIL; 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; pos += ctx.length;
lastSequenceNumber++; lastSequenceNumber++;
@@ -78,7 +83,12 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
lastSequenceNumber = 0; lastSequenceNumber = 0;
if(buf == NULL) return DATA_PARSE_FAIL; 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; pos += ctx.length;
memcpy((uint8_t *) d, buf, pos); memcpy((uint8_t *) d, buf, pos);

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

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

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,3 +1,8 @@
/**
* @copyright Utilitech AS 2023-2026
* License: Fair Source
*
*/
#pragma once #pragma once
#include <stdint.h> #include <stdint.h>
#include <Print.h> #include <Print.h>
@@ -17,6 +22,9 @@
#define AMS_PARTITION_MIN_SPIFFS_SIZE 0x20000 #define AMS_PARTITION_MIN_SPIFFS_SIZE 0x20000
#elif defined(ESP8266) #elif defined(ESP8266)
#include <ESP8266HTTPClient.h> #include <ESP8266HTTPClient.h>
#define AMS_FLASH_SKETCH_SIZE 0xFEFF0
#define AMS_FLASH_OTA_START AMS_FLASH_OTA_SIZE
#endif #endif
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
@@ -36,6 +44,8 @@
#define AMS_UPDATE_ERR_SUCCESS_CONFIRMED 123 #define AMS_UPDATE_ERR_SUCCESS_CONFIRMED 123
#define UPDATE_BUF_SIZE 4096 #define UPDATE_BUF_SIZE 4096
#define UPDATE_MAX_BLOCK_RETRY 25
#define UPDATE_MAX_REBOOT_RETRY 12
class AmsFirmwareUpdater { class AmsFirmwareUpdater {
public: public:
@@ -57,6 +67,13 @@ public:
bool isUpgradeInformationChanged(); bool isUpgradeInformationChanged();
void ackUpgradeInformationChanged(); void ackUpgradeInformationChanged();
void setFirmwareChannel(uint8_t channel) {
if(firmwareChannel != channel) {
firmwareChannel = channel;
lastVersionCheck = 0;
}
}
bool startFirmwareUpload(uint32_t size, const char* version); bool startFirmwareUpload(uint32_t size, const char* version);
bool addFirmwareUploadChunk(uint8_t* buf, size_t length); bool addFirmwareUploadChunk(uint8_t* buf, size_t length);
bool completeFirmwareUpload(uint32_t size); bool completeFirmwareUpload(uint32_t size);
@@ -92,10 +109,11 @@ private:
String md5; String md5;
uint32_t lastVersionCheck = 0; uint32_t lastVersionCheck = 0;
uint8_t firmwareVariant; uint8_t firmwareChannel;
bool autoUpgrade; bool autoUpgrade;
char nextVersion[10]; char nextVersion[17];
void getChannelName(char * buffer);
bool fetchNextVersion(); bool fetchNextVersion();
bool fetchVersionDetails(); bool fetchVersionDetails();

View File

@@ -1,3 +1,8 @@
/**
* @copyright Utilitech AS 2023-2026
* License: Fair Source
*
*/
#include "AmsFirmwareUpdater.h" #include "AmsFirmwareUpdater.h"
#include "AmsStorage.h" #include "AmsStorage.h"
#include "FirmwareVersion.h" #include "FirmwareVersion.h"
@@ -22,7 +27,7 @@ this->debugger = debugger;
this->hw = hw; this->hw = hw;
this->meterState = meterState; this->meterState = meterState;
memset(nextVersion, 0, sizeof(nextVersion)); memset(nextVersion, 0, sizeof(nextVersion));
firmwareVariant = 0; firmwareChannel = 0;
autoUpgrade = false; autoUpgrade = false;
} }
@@ -74,7 +79,7 @@ void AmsFirmwareUpdater::setUpgradeInformation(UpgradeInformation& upinfo) {
#endif #endif
debugger->printf_P(PSTR("Resuming uprade to %s\n"), updateStatus.toVersion); debugger->printf_P(PSTR("Resuming uprade to %s\n"), updateStatus.toVersion);
if(updateStatus.reboot_count++ < 8) { if(updateStatus.reboot_count++ < UPDATE_MAX_REBOOT_RETRY) {
updateStatus.errorCode = AMS_UPDATE_ERR_OK; updateStatus.errorCode = AMS_UPDATE_ERR_OK;
} else { } else {
updateStatus.errorCode = AMS_UPDATE_ERR_REBOOT; updateStatus.errorCode = AMS_UPDATE_ERR_REBOOT;
@@ -92,11 +97,16 @@ void AmsFirmwareUpdater::ackUpgradeInformationChanged() {
} }
float AmsFirmwareUpdater::getProgress() { float AmsFirmwareUpdater::getProgress() {
if(strlen(updateStatus.toVersion) == 0 || updateStatus.size == 0) return -1.0; if(strlen(updateStatus.toVersion) == 0 || updateStatus.size == 0 || updateStatus.errorCode >= AMS_UPDATE_ERR_SUCCESS_SIGNAL) return -1.0;
return min((float) 100.0, ((((float) updateStatus.block_position) * UPDATE_BUF_SIZE) / updateStatus.size) * 100); return min((float) 100.0, ((((float) updateStatus.block_position) * UPDATE_BUF_SIZE) / updateStatus.size) * 100);
} }
void AmsFirmwareUpdater::loop() { void AmsFirmwareUpdater::loop() {
if(millis() < 30000) {
// Wait 30 seconds before starting upgrade. This allows the device to deal with other tasks first
// It will also allow BUS powered devices to reach a stable voltage so that hw->isVoltageOptimal will behave properly
return;
}
if(strlen(updateStatus.toVersion) > 0 && updateStatus.errorCode == AMS_UPDATE_ERR_OK) { if(strlen(updateStatus.toVersion) > 0 && updateStatus.errorCode == AMS_UPDATE_ERR_OK) {
if(!hw->isVoltageOptimal(0.1)) { if(!hw->isVoltageOptimal(0.1)) {
writeUpdateStatus(); writeUpdateStatus();
@@ -124,7 +134,7 @@ void AmsFirmwareUpdater::loop() {
HTTPClient http; HTTPClient http;
start = millis(); start = millis();
if(!fetchFirmwareChunk(http)) { if(!fetchFirmwareChunk(http)) {
if(updateStatus.retry_count++ == 3) { if(updateStatus.retry_count++ > UPDATE_MAX_BLOCK_RETRY) {
updateStatus.errorCode = AMS_UPDATE_ERR_FETCH; updateStatus.errorCode = AMS_UPDATE_ERR_FETCH;
updateStatusChanged = true; updateStatusChanged = true;
} }
@@ -203,15 +213,33 @@ void AmsFirmwareUpdater::loop() {
} }
} }
void AmsFirmwareUpdater::getChannelName(char * buffer) {
switch(firmwareChannel) {
case FIRMWARE_CHANNEL_EARLY:
strcpy(buffer, PSTR("early"));
break;
case FIRMWARE_CHANNEL_RC:
strcpy(buffer, PSTR("rc"));
break;
case FIRMWARE_CHANNEL_SNAPSHOT:
strcpy(buffer, PSTR("snapshot"));
break;
default:
strcpy(buffer, PSTR("stable"));
break;
}
}
bool AmsFirmwareUpdater::fetchNextVersion() { bool AmsFirmwareUpdater::fetchNextVersion() {
HTTPClient http; HTTPClient http;
const char * headerkeys[] = { "x-version" }; const char * headerkeys[] = { "x-version" };
http.collectHeaders(headerkeys, 1); http.collectHeaders(headerkeys, 1);
char firmwareVariant[10] = "stable"; char channel[10] = "";
getChannelName(channel);
char url[128]; char url[128];
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/next"), chipType, firmwareVariant); snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/next"), chipType, channel);
#if defined(ESP8266) #if defined(ESP8266)
WiFiClient client; WiFiClient client;
client.setTimeout(5000); client.setTimeout(5000);
@@ -225,6 +253,16 @@ bool AmsFirmwareUpdater::fetchNextVersion() {
http.setUserAgent("AMS-Firmware-Updater"); http.setUserAgent("AMS-Firmware-Updater");
http.addHeader(F("Cache-Control"), "no-cache"); http.addHeader(F("Cache-Control"), "no-cache");
http.addHeader(F("x-AMS-version"), FirmwareVersion::VersionString); http.addHeader(F("x-AMS-version"), FirmwareVersion::VersionString);
http.addHeader(F("x-AMS-STA-MAC"), WiFi.macAddress());
http.addHeader(F("x-AMS-AP-MAC"), WiFi.softAPmacAddress());
http.addHeader(F("x-AMS-chip-size"), String(ESP.getFlashChipSize()));
http.addHeader(F("x-AMS-board-type"), String(hw->getBoardType(), 10));
if(meterState->getMeterType() != AmsTypeAutodetect) {
http.addHeader(F("x-AMS-meter-mfg"), String(meterState->getMeterType(), 10));
}
if(!meterState->getMeterModel().isEmpty()) {
http.addHeader(F("x-AMS-meter-model"), meterState->getMeterModel());
}
int status = http.GET(); int status = http.GET();
if(status == 204) { if(status == 204) {
String nextVersion = http.header("x-version"); String nextVersion = http.header("x-version");
@@ -248,10 +286,11 @@ bool AmsFirmwareUpdater::fetchVersionDetails() {
const char * headerkeys[] = { "x-size" }; const char * headerkeys[] = { "x-size" };
http.collectHeaders(headerkeys, 1); http.collectHeaders(headerkeys, 1);
char firmwareVariant[10] = "stable"; char channel[10] = "";
getChannelName(channel);
char url[128]; char url[128];
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/details"), chipType, firmwareVariant, updateStatus.toVersion); snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/details"), chipType, channel, updateStatus.toVersion);
#if defined(ESP8266) #if defined(ESP8266)
WiFiClient client; WiFiClient client;
client.setTimeout(5000); client.setTimeout(5000);
@@ -304,10 +343,11 @@ bool AmsFirmwareUpdater::fetchFirmwareChunk(HTTPClient& http) {
char range[24]; char range[24];
snprintf_P(range, 24, PSTR("bytes=%lu-%lu"), start, end); snprintf_P(range, 24, PSTR("bytes=%lu-%lu"), start, end);
char firmwareVariant[10] = "stable"; char channel[10] = "";
getChannelName(channel);
char url[128]; char url[128];
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/chunk"), chipType, firmwareVariant, updateStatus.toVersion); snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/chunk"), chipType, channel, updateStatus.toVersion);
#if defined(ESP8266) #if defined(ESP8266)
WiFiClient client; WiFiClient client;
client.setTimeout(5000); client.setTimeout(5000);
@@ -1128,7 +1168,7 @@ bool AmsFirmwareUpdater::moveLittleFsFromApp1ToNew() {
} }
#elif defined(ESP8266) #elif defined(ESP8266)
uintptr_t AmsFirmwareUpdater::getFirmwareUpdateStart() { uintptr_t AmsFirmwareUpdater::getFirmwareUpdateStart() {
return FS_start - 0x40200000; return (AMS_FLASH_SKETCH_SIZE + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1));
} }
bool AmsFirmwareUpdater::isFlashReadyForNextUpdateVersion(uint32_t size) { bool AmsFirmwareUpdater::isFlashReadyForNextUpdateVersion(uint32_t size) {
@@ -1137,6 +1177,14 @@ bool AmsFirmwareUpdater::isFlashReadyForNextUpdateVersion(uint32_t size) {
#endif #endif
debugger->printf_P(PSTR("Checking if we can upgrade\n")); debugger->printf_P(PSTR("Checking if we can upgrade\n"));
if(FS_PHYS_ADDR < (getFirmwareUpdateStart() + AMS_FLASH_SKETCH_SIZE)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("No room for OTA update\n"));
return false;
}
if(!ESP.checkFlashConfig(false)) { if(!ESP.checkFlashConfig(false)) {
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR)) if (debugger->isActive(RemoteDebug::ERROR))
@@ -1145,24 +1193,12 @@ bool AmsFirmwareUpdater::isFlashReadyForNextUpdateVersion(uint32_t size) {
return false; return false;
} }
//size of current sketch rounded to a sector if(size > AMS_FLASH_SKETCH_SIZE) {
size_t currentSketchSize = (ESP.getSketchSize() + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1));
//size of the update rounded to a sector
size_t roundedSize = (size + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1));
//address of the end of the space available for sketch and update
uintptr_t updateEndAddress = FS_start - 0x40200000;
uintptr_t updateStartAddress = (updateEndAddress > roundedSize) ? (updateEndAddress - roundedSize) : 0;
//make sure that the size of both sketches is less than the total space (updateEndAddress)
if(updateStartAddress < currentSketchSize) {
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR)) if (debugger->isActive(RemoteDebug::ERROR))
#endif #endif
debugger->printf_P(PSTR("New firmware does not fit flash\n")); debugger->printf_P(PSTR("New firmware does not fit flash\n"));
return false; return false;
} }
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO)) if (debugger->isActive(RemoteDebug::INFO))
@@ -1180,14 +1216,28 @@ bool AmsFirmwareUpdater::writeBufferToFlash() {
uint32_t offset = updateStatus.block_position * UPDATE_BUF_SIZE; uint32_t offset = updateStatus.block_position * UPDATE_BUF_SIZE;
uintptr_t currentAddress = getFirmwareUpdateStart() + offset; uintptr_t currentAddress = getFirmwareUpdateStart() + offset;
uint32_t sector = currentAddress/FLASH_SECTOR_SIZE; uint32_t sector = currentAddress/FLASH_SECTOR_SIZE;
if(!ESP.flashEraseSector(sector)) {
if (currentAddress % FLASH_SECTOR_SIZE == 0) {
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR)) if (debugger->isActive(RemoteDebug::DEBUG))
#endif #endif
debugger->printf_P(PSTR("flashEraseSector(%lu) failed\n"), sector); debugger->printf_P(PSTR("flashEraseSector(%lu)\n"), sector);
updateStatus.errorCode = AMS_UPDATE_ERR_ERASE; yield();
return false; if(!ESP.flashEraseSector(sector)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR("flashEraseSector(%lu) failed\n"), sector);
updateStatus.errorCode = AMS_UPDATE_ERR_ERASE;
return false;
}
} }
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("flashWrite(%lu)\n"), sector);
yield();
if(!ESP.flashWrite(currentAddress, buf, UPDATE_BUF_SIZE)) { if(!ESP.flashWrite(currentAddress, buf, UPDATE_BUF_SIZE)) {
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR)) if (debugger->isActive(RemoteDebug::ERROR))

View File

@@ -0,0 +1,14 @@
/**
* @copyright Utilitech AS 2023-2026
* License: Fair Source
*
*/
#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,22 @@
/**
* @copyright Utilitech AS 2023-2026
* License: Fair Source
*
*/
#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

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -14,6 +14,7 @@
#include "EnergyAccounting.h" #include "EnergyAccounting.h"
#include "HwTools.h" #include "HwTools.h"
#include "PriceService.h" #include "PriceService.h"
#include "AmsFirmwareUpdater.h"
#if defined(ESP32) #if defined(ESP32)
#include <esp_task_wdt.h> #include <esp_task_wdt.h>
@@ -22,42 +23,50 @@
class AmsMqttHandler { class AmsMqttHandler {
public: public:
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
AmsMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf) { AmsMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, AmsFirmwareUpdater* updater) {
this->mqttConfig = mqttConfig;
this->mqttConfigChanged = true;
this->debugger = debugger;
this->json = buf;
mqtt.dropOverflow(true);
};
#else #else
AmsMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf) { AmsMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, AmsFirmwareUpdater* updater) {
#endif
this->mqttConfig = mqttConfig; this->mqttConfig = mqttConfig;
this->mqttConfigChanged = true; this->mqttConfigChanged = true;
this->debugger = debugger; this->debugger = debugger;
this->json = buf; this->json = buf;
this->updater = updater;
mqtt.dropOverflow(true); mqtt.dropOverflow(true);
pubTopic = String(mqttConfig.publishTopic);
subTopic = String(mqttConfig.subscribeTopic);
if(subTopic.isEmpty()) subTopic = pubTopic+"/command";
}; };
#endif
void setCaVerification(bool); void setCaVerification(bool);
void setConfig(MqttConfig& mqttConfig); void setConfig(MqttConfig& mqttConfig);
bool connect(); bool connect();
bool defaultSubscribe();
void disconnect(); void disconnect();
lwmqtt_err_t lastError(); lwmqtt_err_t lastError();
bool connected(); bool connected();
bool loop(); bool loop();
bool isRebootSuggested();
virtual uint8_t getFormat() { return 0; }; 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 publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) { return false; };
virtual bool publishTemperatures(AmsConfiguration*, HwTools*) { return false; }; virtual bool publishTemperatures(AmsConfiguration*, HwTools*) { return false; };
virtual bool publishPrices(PriceService* ps) { return false; }; virtual bool publishPrices(PriceService* ps) { return false; };
virtual bool publishSystem(HwTools*, PriceService*, EnergyAccounting*) { 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 void onMessage(String &topic, String &payload) {};
virtual ~AmsMqttHandler() { virtual ~AmsMqttHandler() {
if(mqttSecureClient != NULL) {
mqttSecureClient->stop();
delete mqttSecureClient;
}
if(mqttClient != NULL) { if(mqttClient != NULL) {
mqttClient->stop(); mqttClient->stop();
delete mqttClient; delete mqttClient;
@@ -77,9 +86,17 @@ protected:
bool caVerification = true; bool caVerification = true;
WiFiClient *mqttClient = NULL; WiFiClient *mqttClient = NULL;
WiFiClientSecure *mqttSecureClient = NULL; WiFiClientSecure *mqttSecureClient = NULL;
boolean _connected = false;
char* json; char* json;
uint16_t BufferSize = 2048; uint16_t BufferSize = 2048;
uint64_t lastStateUpdate = 0; uint64_t lastStateUpdate = 0;
uint64_t lastSuccessfulLoop = 0;
String pubTopic;
String subTopic;
AmsFirmwareUpdater* updater = NULL;
bool rebootSuggested = false;
}; };
#endif #endif

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -8,6 +8,7 @@
#include "FirmwareVersion.h" #include "FirmwareVersion.h"
#include "AmsStorage.h" #include "AmsStorage.h"
#include "LittleFS.h" #include "LittleFS.h"
#include "Uptime.h"
void AmsMqttHandler::setCaVerification(bool caVerification) { void AmsMqttHandler::setCaVerification(bool caVerification) {
this->caVerification = caVerification; this->caVerification = caVerification;
@@ -33,15 +34,18 @@ bool AmsMqttHandler::connect() {
if(epoch < FirmwareVersion::BuildEpoch) { if(epoch < FirmwareVersion::BuildEpoch) {
return false; return false;
} }
bool applySslConfiguration = mqttConfigChanged;
if(mqttSecureClient == NULL) { if(mqttSecureClient == NULL) {
mqttSecureClient = new WiFiClientSecure(); mqttSecureClient = new WiFiClientSecure();
#if defined(ESP8266) #if defined(ESP8266)
mqttSecureClient->setBufferSizes(512, 512); mqttSecureClient->setBufferSizes(512, 512);
return false; return false;
#endif #endif
applySslConfiguration = true;
} }
if(mqttConfigChanged) { if(applySslConfiguration) {
if(caVerification && LittleFS.begin()) { if(caVerification && LittleFS.begin()) {
File file; File file;
@@ -99,6 +103,17 @@ bool AmsMqttHandler::connect() {
actualClient = mqttClient; 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; mqttConfigChanged = false;
mqtt.setTimeout(mqttConfig.timeout); mqtt.setTimeout(mqttConfig.timeout);
mqtt.setKeepAlive(mqttConfig.keepalive); mqtt.setKeepAlive(mqttConfig.keepalive);
@@ -117,30 +132,20 @@ bool AmsMqttHandler::connect() {
if ((strlen(mqttConfig.username) == 0 && mqtt.connect(mqttConfig.clientId)) || if ((strlen(mqttConfig.username) == 0 && mqtt.connect(mqttConfig.clientId)) ||
(strlen(mqttConfig.username) > 0 && mqtt.connect(mqttConfig.clientId, mqttConfig.username, mqttConfig.password))) { (strlen(mqttConfig.username) > 0 && mqtt.connect(mqttConfig.clientId, mqttConfig.username, mqttConfig.password))) {
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO)) if (debugger->isActive(RemoteDebug::INFO))
#endif #endif
debugger->printf_P(PSTR("Successfully connected to MQTT\n")); debugger->printf_P(PSTR("Successfully connected to MQTT\n"));
mqtt.onMessage(std::bind(&AmsMqttHandler::onMessage, this, std::placeholders::_1, std::placeholders::_2)); mqtt.onMessage(std::bind(&AmsMqttHandler::onMessage, this, std::placeholders::_1, std::placeholders::_2));
if(strlen(mqttConfig.subscribeTopic) > 0) { _connected = mqtt.publish(statusTopic, "online", true, 0);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR(" Subscribing to [%s]\n"), mqttConfig.subscribeTopic);
if(!mqtt.subscribe(mqttConfig.subscribeTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Unable to subscribe to to [%s]\n"), mqttConfig.subscribeTopic);
}
}
mqtt.publish(statusTopic, "online", true, 0);
mqtt.loop(); mqtt.loop();
defaultSubscribe();
postConnect();
return true; return true;
} else { } else {
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR)) if (debugger->isActive(RemoteDebug::ERROR))
#endif #endif
{ {
debugger->printf_P(PSTR("Failed to connect to MQTT: %d\n"), mqtt.lastError()); debugger->printf_P(PSTR("Failed to connect to MQTT: %d\n"), mqtt.lastError());
#if defined(ESP8266) #if defined(ESP8266)
if(mqttSecureClient) { if(mqttSecureClient) {
@@ -153,9 +158,29 @@ if (debugger->isActive(RemoteDebug::ERROR))
} }
} }
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() { void AmsMqttHandler::disconnect() {
mqtt.disconnect(); mqtt.disconnect();
mqtt.loop(); mqtt.loop();
_connected = false;
delay(10); delay(10);
yield(); yield();
} }
@@ -165,12 +190,25 @@ lwmqtt_err_t AmsMqttHandler::lastError() {
} }
bool AmsMqttHandler::connected() { bool AmsMqttHandler::connected() {
return mqtt.connected(); return _connected && mqtt.connected();
} }
bool AmsMqttHandler::loop() { bool AmsMqttHandler::loop() {
bool ret = mqtt.loop(); uint64_t now = millis64();
delay(10); 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(); yield();
#if defined(ESP32) #if defined(ESP32)
esp_task_wdt_reset(); esp_task_wdt_reset();
@@ -179,3 +217,7 @@ bool AmsMqttHandler::loop() {
#endif #endif
return ret; return ret;
} }
bool AmsMqttHandler::isRebootSuggested() {
return rebootSuggested;
}

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -169,9 +169,24 @@ bool CloudConnector::init() {
} }
void CloudConnector::update(AmsData& data, EnergyAccounting& ea) { void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
if(!config.enabled) return;
unsigned long now = millis(); unsigned long now = millis();
if(now-lastUpdate < config.interval*1000) return; 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(!ESPRandom::isValidV4Uuid(config.clientId)) {
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING)) if (debugger->isActive(RemoteDebug::WARNING))
@@ -179,15 +194,19 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
debugger->printf_P(PSTR("(CloudConnector) Client ID is not valid\n")); debugger->printf_P(PSTR("(CloudConnector) Client ID is not valid\n"));
return; return;
} }
if(data.getListType() < 2) 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(!initialized && !init()) { if(!initialized && !init()) {
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::WARNING)) if (debugger->isActive(RemoteDebug::WARNING))
#endif #endif
debugger->printf_P(PSTR("Unable to initialize cloud connector\n")); debugger->printf_P(PSTR("Unable to initialize cloud connector\n"));
lastUpdate = now;
config.enabled = false;
return; return;
} }
initialized = true; initialized = true;
@@ -202,7 +221,7 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
} }
bool sendData = true; bool sendData = true;
if(lastUpdate == 0) { if(sendFirst) {
seed.clear(); seed.clear();
if(mainFuse > 0 && distributionSystem > 0) { if(mainFuse > 0 && distributionSystem > 0) {
int voltage = distributionSystem == 2 ? 400 : 230; int voltage = distributionSystem == 2 ? 400 : 230;
@@ -460,7 +479,13 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
uint16_t crc = crc16((uint8_t*) clearBuffer, pos); uint16_t crc = crc16((uint8_t*) clearBuffer, pos);
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"crc\":\"%04X\"}"), crc); 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); int ret = mbedtls_rsa_check_pubkey(rsa);
if(ret != 0) { if(ret != 0) {
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
@@ -481,7 +506,7 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
Stream *stream = NULL; Stream *stream = NULL;
if(config.proto == 0) { if(config.proto == 0) {
udp.beginPacket(config.hostname,7443); udp.beginPacket(config.hostname, config.port);
stream = &udp; stream = &udp;
} else if(config.proto == 1) { } else if(config.proto == 1) {
if(!tcp.connected()) { if(!tcp.connected()) {
@@ -493,7 +518,7 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
debugger->printf_P(PSTR("tcp.connect(%s, %d) return code: %d\n"), config.hostname, config.port, ret); debugger->printf_P(PSTR("tcp.connect(%s, %d) return code: %d\n"), config.hostname, config.port, ret);
return; return;
} }
tcp.setTimeout(config.interval * 2); tcp.setTimeout((config.interval * 1000) / 2);
} }
while(tcp.available()) tcp.read(); // Empty incoming buffer while(tcp.available()) tcp.read(); // Empty incoming buffer
stream = &tcp; stream = &tcp;
@@ -521,6 +546,7 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
if(ret == 0) { if(ret == 0) {
if(stream != NULL) { if(stream != NULL) {
stream->write(encryptedBuffer, rsa->len); stream->write(encryptedBuffer, rsa->len);
stream->flush();
} else { } else {
memcpy(httpBuffer + sendBytes, encryptedBuffer, rsa->len); memcpy(httpBuffer + sendBytes, encryptedBuffer, rsa->len);
} }
@@ -565,12 +591,11 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
http.end(); http.end();
} }
} }
lastUpdate = now;
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG)) if (debugger->isActive(RemoteDebug::DEBUG))
#endif #endif
debugger->printf_P(PSTR("%d bytes sent to %s:%d from %s\n"), sendBytes, config.hostname, config.proto == 2 ? 80 : config.port, uuid.c_str()); 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() { void CloudConnector::forceUpdate() {

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -114,6 +114,7 @@ bool EthernetConnectionHandler::connect(NetworkConfig config, SystemConfig sys)
debugger->printf_P(PSTR("Static IP configuration is invalid, not using\n")); debugger->printf_P(PSTR("Static IP configuration is invalid, not using\n"));
} }
} }
this->config = config;
} else { } else {
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR)) if (debugger->isActive(RemoteDebug::ERROR))
@@ -147,6 +148,9 @@ void EthernetConnectionHandler::eventHandler(WiFiEvent_t event, WiFiEventInfo_t
{ {
debugger->printf_P(PSTR("Successfully connected to Ethernet!\n")); debugger->printf_P(PSTR("Successfully connected to Ethernet!\n"));
} }
if(config.ipv6 && !ETH.enableIpV6()) {
debugger->printf_P(PSTR("Unable to enable IPv6\n"));
}
break; break;
case ARDUINO_EVENT_ETH_GOT_IP: case ARDUINO_EVENT_ETH_GOT_IP:
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -19,6 +19,7 @@ bool WiFiAccessPointConnectionHandler::connect(NetworkConfig config, SystemConfi
//wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, 0); // Disable default gw //wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, 0); // Disable default gw
WiFi.mode(WIFI_AP); WiFi.mode(WIFI_AP);
WiFi.persistent(false);
WiFi.softAP(config.ssid, config.psk); WiFi.softAP(config.ssid, config.psk);
dnsServer.setErrorReplyCode(DNSReplyCode::NoError); dnsServer.setErrorReplyCode(DNSReplyCode::NoError);

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -100,6 +100,7 @@ bool WiFiClientConnectionHandler::connect(NetworkConfig config, SystemConfig sys
} }
#endif #endif
WiFi.setAutoReconnect(true); WiFi.setAutoReconnect(true);
WiFi.persistent(false);
this->config = config; this->config = config;
#if defined(ESP32) #if defined(ESP32)
if(begin(config.ssid, config.psk)) { if(begin(config.ssid, config.psk)) {

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -13,11 +13,11 @@
class DomoticzMqttHandler : public AmsMqttHandler { class DomoticzMqttHandler : public AmsMqttHandler {
public: public:
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
DomoticzMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, DomoticzConfig config) : AmsMqttHandler(mqttConfig, debugger, buf) { DomoticzMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, DomoticzConfig config, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
this->config = config; this->config = config;
}; };
#else #else
DomoticzMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, DomoticzConfig config) : AmsMqttHandler(mqttConfig, debugger, buf) { DomoticzMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, DomoticzConfig config, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
this->config = config; this->config = config;
}; };
#endif #endif
@@ -25,7 +25,7 @@ public:
bool publishTemperatures(AmsConfiguration*, HwTools*); bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*); bool publishPrices(PriceService*);
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea); 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); void onMessage(String &topic, String &payload);

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -103,7 +103,7 @@ uint8_t DomoticzMqttHandler::getFormat() {
return 3; return 3;
} }
bool DomoticzMqttHandler::publishRaw(String data) { bool DomoticzMqttHandler::publishRaw(uint8_t* raw, size_t length) {
return false; return false;
} }

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -7,17 +7,39 @@
#ifndef _ENERGYACCOUNTING_H #ifndef _ENERGYACCOUNTING_H
#define _ENERGYACCOUNTING_H #define _ENERGYACCOUNTING_H
#include "Arduino.h"
#include "AmsData.h"
#include "AmsDataStorage.h" #include "AmsDataStorage.h"
#include "PriceService.h" #include "PriceService.h"
struct EnergyAccountingPeak { struct EnergyAccountingPeak {
uint8_t day;
uint8_t hour;
uint16_t value;
};
struct EnergyAccountingPeak6 {
uint8_t day; uint8_t day;
uint16_t value; uint16_t value;
}; };
struct EnergyAccountingData { 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 version;
uint8_t month; uint8_t month;
int32_t costYesterday; int32_t costYesterday;
@@ -29,37 +51,7 @@ struct EnergyAccountingData {
uint32_t lastMonthImport; uint32_t lastMonthImport;
uint32_t lastMonthExport; uint32_t lastMonthExport;
uint8_t lastMonthAccuracy; uint8_t lastMonthAccuracy;
EnergyAccountingPeak peaks[5]; EnergyAccountingPeak6 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;
}; };
struct EnergyAccountingRealtimeData { struct EnergyAccountingRealtimeData {
@@ -89,7 +81,7 @@ public:
void setPriceService(PriceService *ps); void setPriceService(PriceService *ps);
void setTimezone(Timezone*); void setTimezone(Timezone*);
EnergyAccountingConfig* getConfig(); EnergyAccountingConfig* getConfig();
bool update(AmsData* amsData); bool update(time_t now, uint64_t lastUpdatedMillis, uint8_t listType, uint32_t activeImportPower, uint32_t activeExportPower);
bool load(); bool load();
bool save(); bool save();
bool isInitialized(); bool isInitialized();
@@ -124,7 +116,6 @@ public:
void setData(EnergyAccountingData&); void setData(EnergyAccountingData&);
void setCurrency(String currency); void setCurrency(String currency);
float getPriceForHour(uint8_t d, uint8_t h);
private: private:
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
@@ -137,12 +128,12 @@ private:
PriceService *ps = NULL; PriceService *ps = NULL;
EnergyAccountingConfig *config = NULL; EnergyAccountingConfig *config = NULL;
Timezone *tz = 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; EnergyAccountingRealtimeData* realtimeData = NULL;
String currency = ""; String currency = "";
void calcDayCost(); void calcDayCost();
bool updateMax(uint16_t val, uint8_t day); bool updateMax(uint16_t val, uint8_t day, uint8_t hour);
}; };
#endif #endif

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -30,7 +30,7 @@ EnergyAccounting::EnergyAccounting(Stream* Stream, EnergyAccountingRealtimeData*
rtd->lastImportUpdateMillis = 0; rtd->lastImportUpdateMillis = 0;
rtd->lastExportUpdateMillis = 0; rtd->lastExportUpdateMillis = 0;
} }
this->realtimeData = rtd; realtimeData = rtd;
} }
void EnergyAccounting::setup(AmsDataStorage *ds, EnergyAccountingConfig *config) { void EnergyAccounting::setup(AmsDataStorage *ds, EnergyAccountingConfig *config) {
@@ -54,9 +54,8 @@ bool EnergyAccounting::isInitialized() {
return this->init; return this->init;
} }
bool EnergyAccounting::update(AmsData* amsData) { bool EnergyAccounting::update(time_t now, uint64_t lastUpdatedMillis, uint8_t listType, uint32_t activeImportPower, uint32_t activeExportPower) {
if(config == NULL) return false; if(config == NULL) return false;
time_t now = time(nullptr);
if(now < FirmwareVersion::BuildEpoch) return false; if(now < FirmwareVersion::BuildEpoch) return false;
if(tz == NULL) { if(tz == NULL) {
return false; return false;
@@ -67,59 +66,55 @@ bool EnergyAccounting::update(AmsData* amsData) {
breakTime(tz->toLocal(now), local); breakTime(tz->toLocal(now), local);
if(!init) { if(!init) {
this->realtimeData->lastImportUpdateMillis = 0; realtimeData->lastImportUpdateMillis = 0;
this->realtimeData->lastExportUpdateMillis = 0; realtimeData->lastExportUpdateMillis = 0;
this->realtimeData->currentHour = local.Hour; realtimeData->currentHour = local.Hour;
this->realtimeData->currentDay = local.Day; realtimeData->currentDay = local.Day;
if(!load()) { if(!load()) {
data = { 6, local.Month, data = { 7, local.Month,
0, 0, 0, // Cost 0, 0, 0, 0, // Cost
0, 0, 0, // Income 0, 0, 0, 0, // Income
0, 0, 0, // Last month import, export and accuracy 0, 0, 0, // Last month import, export and accuracy
0, 0, // Peak 1 0, 0, 0, // Peak 1
0, 0, // Peak 2 0, 0, 0, // Peak 2
0, 0, // Peak 3 0, 0, 0, // Peak 3
0, 0, // Peak 4 0, 0, 0, // Peak 4
0, 0 // Peak 5 0, 0, 0 // Peak 5
}; };
} }
init = true; init = true;
} }
float importPrice = getPriceForHour(PRICE_DIRECTION_IMPORT, 0); if(!initPrice && ps != NULL && ps->hasPrice()) {
if(!initPrice && importPrice != PRICE_NO_VALUE) {
calcDayCost(); calcDayCost();
} }
if(local.Hour != this->realtimeData->currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) { if(local.Hour != realtimeData->currentHour && (listType >= 3 || local.Minute == 1)) {
tmElements_t oneHrAgo, oneHrAgoLocal; tmElements_t oneHrAgo, oneHrAgoLocal;
breakTime(now-3600, oneHrAgo); breakTime(now-3600, oneHrAgo);
uint16_t val = round(ds->getHourImport(oneHrAgo.Hour) / 10.0); uint16_t val = round(ds->getHourImport(oneHrAgo.Hour) / 10.0);
breakTime(tz->toLocal(now-3600), oneHrAgoLocal); 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 realtimeData->currentHour = local.Hour; // Need to be defined here so that day cost is correctly calculated
if(local.Hour > 0) {
calcDayCost();
}
this->realtimeData->use = 0; realtimeData->use = 0;
this->realtimeData->produce = 0; realtimeData->produce = 0;
this->realtimeData->costHour = 0; realtimeData->costHour = 0;
this->realtimeData->incomeHour = 0; realtimeData->incomeHour = 0;
uint8_t prevDay = this->realtimeData->currentDay; uint8_t prevDay = realtimeData->currentDay;
if(local.Day != this->realtimeData->currentDay) { if(local.Day != realtimeData->currentDay) {
data.costYesterday = this->realtimeData->costDay * 100; data.costYesterday = realtimeData->costDay * 100;
data.costThisMonth += this->realtimeData->costDay * 100; data.costThisMonth += realtimeData->costDay * 100;
this->realtimeData->costDay = 0; realtimeData->costDay = 0;
data.incomeYesterday = this->realtimeData->incomeDay * 100; data.incomeYesterday = realtimeData->incomeDay * 100;
data.incomeThisMonth += this->realtimeData->incomeDay * 100; data.incomeThisMonth += realtimeData->incomeDay * 100;
this->realtimeData->incomeDay = 0; realtimeData->incomeDay = 0;
this->realtimeData->currentDay = local.Day; realtimeData->currentDay = local.Day;
ret = true; ret = true;
} }
@@ -149,42 +144,49 @@ bool EnergyAccounting::update(AmsData* amsData) {
data.lastMonthAccuracy = accuracy; data.lastMonthAccuracy = accuracy;
data.month = local.Month; data.month = local.Month;
this->realtimeData->currentThresholdIdx = 0; realtimeData->currentThresholdIdx = 0;
ret = true; ret = true;
} }
if(ret) {
data.costToday = realtimeData->costDay * 100;
data.incomeToday = realtimeData->incomeDay * 100;
data.lastUpdated = now;
}
} }
if(this->realtimeData->lastImportUpdateMillis < amsData->getLastUpdateMillis()) { if(realtimeData->lastImportUpdateMillis < lastUpdatedMillis) {
unsigned long ms = amsData->getLastUpdateMillis() - this->realtimeData->lastImportUpdateMillis; unsigned long ms = lastUpdatedMillis - realtimeData->lastImportUpdateMillis;
float kwhi = (amsData->getActiveImportPower() * (((float) ms) / 3600000.0)) / 1000.0; float kwhi = (activeImportPower * (((float) ms) / 3600000.0)) / 1000.0;
if(kwhi > 0) { if(kwhi > 0) {
this->realtimeData->use += kwhi; realtimeData->use += kwhi;
float importPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_IMPORT);
if(importPrice != PRICE_NO_VALUE) { if(importPrice != PRICE_NO_VALUE) {
float cost = importPrice * kwhi; float cost = importPrice * kwhi;
this->realtimeData->costHour += cost; realtimeData->costHour += cost;
this->realtimeData->costDay += cost; realtimeData->costDay += cost;
} }
} }
this->realtimeData->lastImportUpdateMillis = amsData->getLastUpdateMillis(); realtimeData->lastImportUpdateMillis = lastUpdatedMillis;
} }
if(amsData->getListType() > 1 && this->realtimeData->lastExportUpdateMillis < amsData->getLastUpdateMillis()) { if(listType > 1 && realtimeData->lastExportUpdateMillis < lastUpdatedMillis) {
unsigned long ms = amsData->getLastUpdateMillis() - this->realtimeData->lastExportUpdateMillis; unsigned long ms = lastUpdatedMillis - realtimeData->lastExportUpdateMillis;
float kwhe = (amsData->getActiveExportPower() * (((float) ms) / 3600000.0)) / 1000.0; float kwhe = (activeExportPower * (((float) ms) / 3600000.0)) / 1000.0;
if(kwhe > 0) { if(kwhe > 0) {
this->realtimeData->produce += kwhe; realtimeData->produce += kwhe;
float exportPrice = getPriceForHour(PRICE_DIRECTION_EXPORT, 0); float exportPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_EXPORT);
if(exportPrice != PRICE_NO_VALUE) { if(exportPrice != PRICE_NO_VALUE) {
float income = exportPrice * kwhe; float income = exportPrice * kwhe;
this->realtimeData->incomeHour += income; realtimeData->incomeHour += income;
this->realtimeData->incomeDay += income; realtimeData->incomeDay += income;
} }
} }
this->realtimeData->lastExportUpdateMillis = amsData->getLastUpdateMillis(); realtimeData->lastExportUpdateMillis = lastUpdatedMillis;
} }
if(config != NULL) { 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; return ret;
@@ -192,28 +194,36 @@ bool EnergyAccounting::update(AmsData* amsData) {
void EnergyAccounting::calcDayCost() { void EnergyAccounting::calcDayCost() {
time_t now = time(nullptr); time_t now = time(nullptr);
tmElements_t local, utc; tmElements_t local, utc, lastUpdateUtc;
if(tz == NULL) return; if(tz == NULL) return;
breakTime(tz->toLocal(now), local); breakTime(tz->toLocal(now), local);
if(ps == NULL) return;
if(getPriceForHour(PRICE_DIRECTION_IMPORT, 0) != PRICE_NO_VALUE) { if(ps->hasPrice()) {
if(initPrice) { breakTime(data.lastUpdated, lastUpdateUtc);
this->realtimeData->costDay = 0; uint8_t calcFromHour = 0;
this->realtimeData->incomeDay = 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); 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) { if(priceIn != PRICE_NO_VALUE) {
int16_t wh = ds->getHourImport(utc.Hour); 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) { if(priceOut != PRICE_NO_VALUE) {
int16_t wh = ds->getHourExport(utc.Hour); int16_t wh = ds->getHourExport(utc.Hour);
this->realtimeData->incomeDay += priceOut * (wh / 1000.0); realtimeData->incomeDay += priceOut * (wh / 1000.0);
} }
} }
initPrice = true; initPrice = true;
@@ -221,7 +231,7 @@ void EnergyAccounting::calcDayCost() {
} }
float EnergyAccounting::getUseThisHour() { float EnergyAccounting::getUseThisHour() {
return this->realtimeData->use; return realtimeData->use;
} }
float EnergyAccounting::getUseToday() { float EnergyAccounting::getUseToday() {
@@ -231,7 +241,7 @@ float EnergyAccounting::getUseToday() {
if(now < FirmwareVersion::BuildEpoch) return 0.0; if(now < FirmwareVersion::BuildEpoch) return 0.0;
tmElements_t utc, local; tmElements_t utc, local;
breakTime(tz->toLocal(now), 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); breakTime(now - ((local.Hour - i) * 3600), utc);
ret += ds->getHourImport(utc.Hour) / 1000.0; ret += ds->getHourImport(utc.Hour) / 1000.0;
} }
@@ -242,18 +252,20 @@ float EnergyAccounting::getUseThisMonth() {
time_t now = time(nullptr); time_t now = time(nullptr);
if(now < FirmwareVersion::BuildEpoch) return 0.0; if(now < FirmwareVersion::BuildEpoch) return 0.0;
float ret = 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; ret += ds->getDayImport(i) / 1000.0;
} }
return ret + getUseToday(); return ret + getUseToday();
} }
float EnergyAccounting::getUseLastMonth() { 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() { float EnergyAccounting::getProducedThisHour() {
return this->realtimeData->produce; return realtimeData->produce;
} }
float EnergyAccounting::getProducedToday() { float EnergyAccounting::getProducedToday() {
@@ -263,7 +275,7 @@ float EnergyAccounting::getProducedToday() {
if(now < FirmwareVersion::BuildEpoch) return 0.0; if(now < FirmwareVersion::BuildEpoch) return 0.0;
tmElements_t utc, local; tmElements_t utc, local;
breakTime(tz->toLocal(now), 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); breakTime(now - ((local.Hour - i) * 3600), utc);
ret += ds->getHourExport(utc.Hour) / 1000.0; ret += ds->getHourExport(utc.Hour) / 1000.0;
} }
@@ -274,22 +286,24 @@ float EnergyAccounting::getProducedThisMonth() {
time_t now = time(nullptr); time_t now = time(nullptr);
if(now < FirmwareVersion::BuildEpoch) return 0.0; if(now < FirmwareVersion::BuildEpoch) return 0.0;
float ret = 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; ret += ds->getDayExport(i) / 1000.0;
} }
return ret + getProducedToday(); return ret + getProducedToday();
} }
float EnergyAccounting::getProducedLastMonth() { 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() { float EnergyAccounting::getCostThisHour() {
return this->realtimeData->costHour; return realtimeData->costHour;
} }
float EnergyAccounting::getCostToday() { float EnergyAccounting::getCostToday() {
return this->realtimeData->costDay; return realtimeData->costDay;
} }
float EnergyAccounting::getCostYesterday() { float EnergyAccounting::getCostYesterday() {
@@ -305,11 +319,11 @@ float EnergyAccounting::getCostLastMonth() {
} }
float EnergyAccounting::getIncomeThisHour() { float EnergyAccounting::getIncomeThisHour() {
return this->realtimeData->incomeHour; return realtimeData->incomeHour;
} }
float EnergyAccounting::getIncomeToday() { float EnergyAccounting::getIncomeToday() {
return this->realtimeData->incomeDay; return realtimeData->incomeDay;
} }
float EnergyAccounting::getIncomeYesterday() { float EnergyAccounting::getIncomeYesterday() {
@@ -327,7 +341,7 @@ float EnergyAccounting::getIncomeLastMonth() {
uint8_t EnergyAccounting::getCurrentThreshold() { uint8_t EnergyAccounting::getCurrentThreshold() {
if(config == NULL) if(config == NULL)
return 0; return 0;
return config->thresholds[this->realtimeData->currentThresholdIdx]; return config->thresholds[realtimeData->currentThresholdIdx];
} }
float EnergyAccounting::getMonthMax() { float EnergyAccounting::getMonthMax() {
@@ -407,85 +421,31 @@ bool EnergyAccounting::load() {
char buf[file.size()]; char buf[file.size()];
file.readBytes(buf, file.size()); file.readBytes(buf, file.size());
if(buf[0] == 6) { if(buf[0] == 7) {
EnergyAccountingData* data = (EnergyAccountingData*) buf; EnergyAccountingData* data = (EnergyAccountingData*) buf;
memcpy(&this->data, data, sizeof(this->data)); memcpy(&this->data, data, sizeof(this->data));
ret = true; ret = true;
} else if(buf[0] == 5) { } else if(buf[0] == 6) {
EnergyAccountingData5* data = (EnergyAccountingData5*) buf; EnergyAccountingData6* data = (EnergyAccountingData6*) buf;
this->data = { 6, data->month, this->data = { 7, data->month,
((uint32_t) data->costYesterday) * 10, 0, // Cost today
((uint32_t) data->costThisMonth) * 100, data->costYesterday,
((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,
data->costThisMonth, data->costThisMonth,
data->costLastMonth, data->costLastMonth,
0,0,0, // Income from production 0, // Income today
0,0,0, // Last month import, export and accuracy data->incomeYesterday,
data->peaks[0].day, data->peaks[0].value, data->incomeThisMonth,
data->peaks[1].day, data->peaks[1].value, data->incomeLastMonth,
data->peaks[2].day, data->peaks[2].value, data->lastMonthImport,
data->peaks[3].day, data->peaks[3].value, data->lastMonthExport,
data->peaks[4].day, data->peaks[4].value 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; 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(); file.close();
@@ -518,11 +478,12 @@ void EnergyAccounting::setData(EnergyAccountingData& data) {
this->data = 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++) { for(uint8_t i = 0; i < 5; i++) {
if(data.peaks[i].day == day || data.peaks[i].day == 0) { if(data.peaks[i].day == day || data.peaks[i].day == 0) {
if(val > data.peaks[i].value) { if(val > data.peaks[i].value) {
data.peaks[i].day = day; data.peaks[i].day = day;
data.peaks[i].hour = hour;
data.peaks[i].value = val; data.peaks[i].value = val;
return true; return true;
} }
@@ -550,8 +511,3 @@ bool EnergyAccounting::updateMax(uint16_t val, uint8_t day) {
void EnergyAccounting::setCurrency(String currency) { void EnergyAccounting::setCurrency(String currency) {
this->currency = 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

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -15,30 +15,31 @@
class HomeAssistantMqttHandler : public AmsMqttHandler { class HomeAssistantMqttHandler : public AmsMqttHandler {
public: public:
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
HomeAssistantMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) { HomeAssistantMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater, char* hostname) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
#else #else
HomeAssistantMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) { HomeAssistantMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater, char* hostname) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
#endif #endif
this->boardType = boardType; this->boardType = boardType;
this->hw = hw; this->hw = hw;
setHomeAssistantConfig(config); setHomeAssistantConfig(config, hostname);
}; };
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps); bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps);
bool publishTemperatures(AmsConfiguration*, HwTools*); bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*); bool publishPrices(PriceService*);
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea); 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); void onMessage(String &topic, String &payload);
uint8_t getFormat(); uint8_t getFormat();
void setHomeAssistantConfig(HomeAssistantConfig config); void setHomeAssistantConfig(HomeAssistantConfig config, char* hostname);
private: private:
uint8_t boardType; uint8_t boardType;
String topic;
String deviceName; String deviceName;
String deviceModel; String deviceModel;
String deviceUid; String deviceUid;
@@ -46,12 +47,13 @@ private:
String deviceUrl; String deviceUrl;
String statusTopic; String statusTopic;
String discoveryTopic; String sensorTopic;
String updateTopic;
String sensorNamePrefix; String sensorNamePrefix;
bool l1Init, l2Init, l2eInit, l3Init, l3eInit, l4Init, l4eInit, rtInit, rteInit, pInit, sInit, rInit; bool l1Init, l2Init, l2eInit, l3Init, l3eInit, l4Init, l4eInit, rtInit, rteInit, pInit, sInit, rInit, fInit, dInit;
bool tInit[32] = {false}; bool tInit[32] = {false};
bool prInit[38] = {false}; uint8_t priceImportInit = 0, priceExportInit = 0;
uint32_t lastThresholdPublish = 0; uint32_t lastThresholdPublish = 0;
HwTools* hw; HwTools* hw;
@@ -77,6 +79,7 @@ private:
void publishPriceSensors(PriceService* ps); void publishPriceSensors(PriceService* ps);
void publishSystemSensors(); void publishSystemSensors();
void publishThresholdSensors(); void publishThresholdSensors();
void toJsonIsoTimestamp(time_t t, char* buf, size_t buflen);
String boardTypeToString(uint8_t b) { String boardTypeToString(uint8_t b) {
switch(b) { switch(b) {

View File

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

View File

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

View File

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

View File

@@ -12,5 +12,5 @@
"U1" : %.2f, "U1" : %.2f,
"U2" : %.2f, "U2" : %.2f,
"U3" : %.2f, "U3" : %.2f,
"t" : "%s" "t" : %s
} }

View File

@@ -28,5 +28,5 @@
"tPO1" : %.3f, "tPO1" : %.3f,
"tPO2" : %.3f, "tPO2" : %.3f,
"tPO3" : %.3f, "tPO3" : %.3f,
"t" : "%s" "t" : %s
} }

View File

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

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -19,10 +19,8 @@
#include <esp_task_wdt.h> #include <esp_task_wdt.h>
#endif #endif
void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config) { void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config, char* hostname) {
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = false; l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = fInit = dInit = false;
topic = String(mqttConfig.publishTopic);
if(strlen(config.discoveryNameTag) > 0) { if(strlen(config.discoveryNameTag) > 0) {
snprintf_P(json, 128, PSTR("AMS reader (%s)"), config.discoveryNameTag); snprintf_P(json, 128, PSTR("AMS reader (%s)"), config.discoveryNameTag);
@@ -30,21 +28,18 @@ void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config
snprintf_P(json, 128, PSTR("[%s] "), config.discoveryNameTag); snprintf_P(json, 128, PSTR("[%s] "), config.discoveryNameTag);
sensorNamePrefix = String(json); sensorNamePrefix = String(json);
} else { } else {
deviceName = F("AMS reader"); snprintf_P(json, 128, PSTR("AMS reader"));
deviceName = String(json);
sensorNamePrefix = ""; sensorNamePrefix = "";
} }
deviceModel = boardTypeToString(boardType); deviceModel = boardTypeToString(boardType);
manufacturer = boardManufacturerToString(boardType); manufacturer = boardManufacturerToString(boardType);
char hostname[32];
#if defined(ESP8266)
strcpy(hostname, WiFi.hostname().c_str());
#elif defined(ESP32)
strcpy(hostname, WiFi.getHostname());
#endif
stripNonAscii((uint8_t*) hostname, 32, false);
deviceUid = String(hostname); // Maybe configurable in the future? 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(strlen(config.discoveryHostname) > 0) {
if(strncmp_P(config.discoveryHostname, PSTR("http"), 4) == 0) { if(strncmp_P(config.discoveryHostname, PSTR("http"), 4) == 0) {
@@ -58,21 +53,42 @@ void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config
deviceUrl = String(json); deviceUrl = String(json);
} }
if(strlen(config.discoveryPrefix) > 0) { if(strlen(config.discoveryPrefix) == 0) {
snprintf_P(json, 128, PSTR("%s/status"), config.discoveryPrefix); snprintf_P(config.discoveryPrefix, 64, PSTR("homeassistant"));
statusTopic = String(json);
snprintf_P(json, 128, PSTR("%s/sensor/"), config.discoveryPrefix);
discoveryTopic = String(json);
} else {
statusTopic = F("homeassistant/status");
discoveryTopic = F("homeassistant/sensor/");
} }
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()); 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) { bool HomeAssistantMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
if(topic.isEmpty() || !mqtt.connected()) if(pubTopic.isEmpty() || !connected())
return false; return false;
if(time(nullptr) < FirmwareVersion::BuildEpoch) if(time(nullptr) < FirmwareVersion::BuildEpoch)
@@ -117,15 +133,10 @@ bool HomeAssistantMqttHandler::publishList1(AmsData* data, EnergyAccounting* ea)
publishList1Sensors(); publishList1Sensors();
char pt[24]; char pt[24];
memset(pt, 0, 24); toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
if(data->getPackageTimestamp() > 0) {
tmElements_t tm;
breakTime(data->getPackageTimestamp(), tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
snprintf_P(json, BufferSize, HA1_JSON, data->getActiveImportPower(), pt); snprintf_P(json, BufferSize, HA1_JSON, data->getActiveImportPower(), pt);
return mqtt.publish(topic + "/power", json); return mqtt.publish(pubTopic + "/power", json);
} }
bool HomeAssistantMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea) { bool HomeAssistantMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea) {
@@ -133,12 +144,7 @@ bool HomeAssistantMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea)
if(data->getActiveExportPower() > 0) publishList2ExportSensors(); if(data->getActiveExportPower() > 0) publishList2ExportSensors();
char pt[24]; char pt[24];
memset(pt, 0, 24); toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
if(data->getPackageTimestamp() > 0) {
tmElements_t tm;
breakTime(data->getPackageTimestamp(), tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
snprintf_P(json, BufferSize, HA3_JSON, snprintf_P(json, BufferSize, HA3_JSON,
data->getListId().c_str(), data->getListId().c_str(),
@@ -156,7 +162,7 @@ bool HomeAssistantMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea)
data->getL3Voltage(), data->getL3Voltage(),
pt pt
); );
return mqtt.publish(topic + "/power", json); return mqtt.publish(pubTopic + "/power", json);
} }
bool HomeAssistantMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea) { bool HomeAssistantMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea) {
@@ -164,20 +170,11 @@ bool HomeAssistantMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea)
if(data->getActiveExportCounter() > 0.0) publishList3ExportSensors(); if(data->getActiveExportCounter() > 0.0) publishList3ExportSensors();
char mt[24]; char mt[24];
memset(mt, 0, 24); toJsonIsoTimestamp(data->getMeterTimestamp(), mt, sizeof(mt));
if(data->getMeterTimestamp() > 0) {
tmElements_t tm;
breakTime(data->getMeterTimestamp(), tm);
sprintf_P(mt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
char pt[24]; char pt[24];
memset(pt, 0, 24); memset(pt, 0, 24);
if(data->getPackageTimestamp() > 0) { toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
tmElements_t tm;
breakTime(data->getPackageTimestamp(), tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
snprintf_P(json, BufferSize, HA2_JSON, snprintf_P(json, BufferSize, HA2_JSON,
data->getActiveImportCounter(), data->getActiveImportCounter(),
@@ -187,7 +184,7 @@ bool HomeAssistantMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea)
mt, mt,
pt pt
); );
return mqtt.publish(topic + "/energy", json); return mqtt.publish(pubTopic + "/energy", json);
} }
bool HomeAssistantMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea) { bool HomeAssistantMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea) {
@@ -195,12 +192,7 @@ bool HomeAssistantMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea)
if(data->getL1ActiveExportPower() > 0 || data->getL2ActiveExportPower() > 0 || data->getL3ActiveExportPower() > 0) publishList4ExportSensors(); if(data->getL1ActiveExportPower() > 0 || data->getL2ActiveExportPower() > 0 || data->getL3ActiveExportPower() > 0) publishList4ExportSensors();
char pt[24]; char pt[24];
memset(pt, 0, 24); toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
if(data->getPackageTimestamp() > 0) {
tmElements_t tm;
breakTime(data->getPackageTimestamp(), tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
snprintf_P(json, BufferSize, HA4_JSON, snprintf_P(json, BufferSize, HA4_JSON,
data->getListId().c_str(), data->getListId().c_str(),
@@ -234,7 +226,7 @@ bool HomeAssistantMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea)
data->getL3ActiveExportCounter(), data->getL3ActiveExportCounter(),
pt pt
); );
return mqtt.publish(topic + "/power", json); return mqtt.publish(pubTopic + "/power", json);
} }
String HomeAssistantMqttHandler::getMeterModel(AmsData* data) { String HomeAssistantMqttHandler::getMeterModel(AmsData* data) {
@@ -290,18 +282,13 @@ bool HomeAssistantMqttHandler::publishRealtime(AmsData* data, EnergyAccounting*
time_t now = time(nullptr); time_t now = time(nullptr);
char pt[24]; char pt[24];
memset(pt, 0, 24); toJsonIsoTimestamp(now, pt, sizeof(pt));
if(now > 0) { pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
tmElements_t tm;
breakTime(now, tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":\"%s\""), pt);
json[pos++] = '}'; json[pos++] = '}';
json[pos] = '\0'; json[pos] = '\0';
return mqtt.publish(topic + "/realtime", json); return mqtt.publish(pubTopic + "/realtime", json);
} }
bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) { bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
@@ -326,24 +313,19 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
time_t now = time(nullptr); time_t now = time(nullptr);
char pt[24]; char pt[24];
memset(pt, 0, 24); toJsonIsoTimestamp(now, pt, sizeof(pt));
if(now > 0) { pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
tmElements_t tm;
breakTime(now, tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":\"%s\""), pt);
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("}")); pos += snprintf_P(json+pos, BufferSize-pos, PSTR("}"));
bool ret = mqtt.publish(topic + "/temperatures", json); bool ret = mqtt.publish(pubTopic + "/temperatures", json);
loop(); loop();
return ret; return ret;
} }
bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) { bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
if(topic.isEmpty() || !mqtt.connected()) if(pubTopic.isEmpty() || !connected())
return false; return false;
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE) if(!ps->hasPrice())
return false; return false;
publishPriceSensors(ps); publishPriceSensors(ps);
@@ -356,7 +338,7 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
float values[38]; float values[38];
for(int i = 0;i < 38; i++) values[i] = PRICE_NO_VALUE; for(int i = 0;i < 38; i++) values[i] = PRICE_NO_VALUE;
for(uint8_t i = 0; i < 38; i++) { 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; values[i] = val;
if(val == PRICE_NO_VALUE) break; if(val == PRICE_NO_VALUE) break;
@@ -404,37 +386,63 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
memset(ts1hr, 0, 24); memset(ts1hr, 0, 24);
if(min1hrIdx > -1) { if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx); time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
tmElements_t tm; tmElements_t tm;
breakTime(ts, 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]; char ts3hr[24];
memset(ts3hr, 0, 24); memset(ts3hr, 0, 24);
if(min3hrIdx > -1) { if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx); time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
tmElements_t tm; tmElements_t tm;
breakTime(ts, 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]; char ts6hr[24];
memset(ts6hr, 0, 24); memset(ts6hr, 0, 24);
if(min6hrIdx > -1) { if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx); time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
tmElements_t tm; tmElements_t tm;
breakTime(ts, 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()); uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{\"import\":["), WiFi.macAddress().c_str());
for(uint8_t i = 0;i < 38; i++) {
if(values[i] == PRICE_NO_VALUE) { uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex();
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%d\":null,"), i); 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 { } 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);
}
} }
} }
pos += 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, min == INT16_MAX ? 0.0 : min,
max == INT16_MIN ? 0.0 : max, max == INT16_MIN ? 0.0 : max,
ts1hr, ts1hr,
@@ -442,32 +450,20 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
ts6hr ts6hr
); );
float val = ps->getValueForHour(PRICE_DIRECTION_EXPORT, now, 0); char pt[24];
if(val == PRICE_NO_VALUE) { toJsonIsoTimestamp(now, pt, sizeof(pt));
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"exportprices\":{\"0\":null}")); pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
} else {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"exportprices\":{\"0\":%.4f}"), val);
}
char pt[24];
memset(pt, 0, 24);
if(now > 0) {
tmElements_t tm;
breakTime(now, tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":\"%s\""), pt);
json[pos++] = '}'; json[pos++] = '}';
json[pos] = '\0'; json[pos] = '\0';
bool ret = mqtt.publish(topic + "/prices", json, true, 0); bool ret = mqtt.publish(pubTopic + "/prices", json, true, 0);
loop(); loop();
return ret; return ret;
} }
bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) { bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) {
if(topic.isEmpty() || !mqtt.connected()) if(pubTopic.isEmpty() || !connected())
return false; return false;
publishSystemSensors(); publishSystemSensors();
@@ -475,14 +471,9 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, Ener
time_t now = time(nullptr); time_t now = time(nullptr);
char pt[24]; char pt[24];
memset(pt, 0, 24); toJsonIsoTimestamp(now, pt, sizeof(pt));
if(now > 0) {
tmElements_t tm;
breakTime(now, tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\",\"t\":\"%s\"}"), 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(), WiFi.macAddress().c_str(),
mqttConfig.clientId, mqttConfig.clientId,
(uint32_t) (millis64()/1000), (uint32_t) (millis64()/1000),
@@ -492,24 +483,28 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, Ener
FirmwareVersion::VersionString, FirmwareVersion::VersionString,
pt pt
); );
bool ret = mqtt.publish(topic + "/state", json); bool ret = mqtt.publish(pubTopic + "/state", json);
loop(); loop();
return ret; return ret;
} }
void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor sensor) { void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor sensor) {
String uid = String(sensor.path); String uid;
uid.replace(".", ""); if(strlen(sensor.uid) > 0) {
uid.replace("[", ""); uid = String(sensor.uid);
uid.replace("]", ""); } else {
uid.replace("'", ""); uid = String(sensor.path);
uid.replace(".", "");
uid.replace("[", "");
uid.replace("]", "");
uid.replace("'", "");
}
snprintf_P(json, BufferSize, HADISCOVER_JSON, snprintf_P(json, BufferSize, HADISCOVER_JSON,
sensorNamePrefix.c_str(), sensorNamePrefix.c_str(),
sensor.name, sensor.name,
mqttConfig.publishTopic, sensor.topic, mqttConfig.publishTopic, sensor.topic,
deviceUid.c_str(), uid.c_str(), deviceUid.c_str(), uid.c_str(),
deviceUid.c_str(), uid.c_str(), deviceUid.c_str(), uid.c_str(),
sensor.uom,
sensor.path, sensor.path,
sensor.ttl, sensor.ttl,
deviceUid.c_str(), deviceUid.c_str(),
@@ -518,14 +513,21 @@ void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor sensor) {
FirmwareVersion::VersionString, FirmwareVersion::VersionString,
manufacturer.c_str(), manufacturer.c_str(),
deviceUrl.c_str(), deviceUrl.c_str(),
strlen_P(sensor.devcl) > 0 ? ",\"dev_cla\":\"" : "", strlen_P(sensor.devcl) > 0 ? ",\"dev_cla\":\"" : "",
strlen_P(sensor.devcl) > 0 ? (char *) FPSTR(sensor.devcl) : "", strlen_P(sensor.devcl) > 0 ? (char *) FPSTR(sensor.devcl) : "",
strlen_P(sensor.devcl) > 0 ? "\"" : "", strlen_P(sensor.devcl) > 0 ? "\"" : "",
strlen_P(sensor.stacl) > 0 ? ",\"stat_cla\":\"" : "", strlen_P(sensor.stacl) > 0 ? ",\"stat_cla\":\"" : "",
strlen_P(sensor.stacl) > 0 ? (char *) FPSTR(sensor.stacl) : "", 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(); loop();
} }
@@ -614,7 +616,8 @@ void HomeAssistantMqttHandler::publishRealtimeSensors(EnergyAccounting* ea, Pric
RealtimePeakSensor.ttl, RealtimePeakSensor.ttl,
RealtimePeakSensor.uom, RealtimePeakSensor.uom,
RealtimePeakSensor.devcl, RealtimePeakSensor.devcl,
RealtimePeakSensor.stacl RealtimePeakSensor.stacl,
RealtimePeakSensor.uid
}; };
publishSensor(sensor); publishSensor(sensor);
} }
@@ -653,7 +656,8 @@ void HomeAssistantMqttHandler::publishTemperatureSensor(uint8_t index, String id
TemperatureSensor.ttl, TemperatureSensor.ttl,
TemperatureSensor.uom, TemperatureSensor.uom,
TemperatureSensor.devcl, TemperatureSensor.devcl,
TemperatureSensor.stacl TemperatureSensor.stacl,
TemperatureSensor.uid
}; };
publishSensor(sensor); publishSensor(sensor);
tInit[index] = true; tInit[index] = true;
@@ -673,45 +677,96 @@ void HomeAssistantMqttHandler::publishPriceSensors(PriceService* ps) {
} }
pInit = true; 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]; uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex();
snprintf(name, strlen(PriceSensor.name)+2, PriceSensor.name, i, i == 1 ? "hour" : "hours"); uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
char path[strlen(PriceSensor.path)+1];
snprintf(path, strlen(PriceSensor.path)+1, PriceSensor.path, i); if(priceImportInit < numberOfPoints-currentPricePointIndex) {
HomeAssistantSensor sensor = { uint8_t importPriceSensorNo = 0;
i == 0 ? "Price current hour" : name, for(int pricePointIndex = currentPricePointIndex; pricePointIndex < numberOfPoints; pricePointIndex++) {
PriceSensor.topic, float val = ps->getPricePoint(PRICE_DIRECTION_IMPORT, pricePointIndex);
path, if(val == PRICE_NO_VALUE) break;
PriceSensor.ttl, if(importPriceSensorNo < priceImportInit) {
uom.c_str(), importPriceSensorNo++;
PriceSensor.devcl, continue;
i == 0 ? "total" : PriceSensor.stacl }
};
publishSensor(sensor); uint8_t resolution = ps->getResolutionInMinutes();
prInit[i] = true;
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++;
}
} }
float exportPrice = ps->getValueForHour(PRICE_DIRECTION_EXPORT, 0); if(priceExportInit < numberOfPoints-currentPricePointIndex) {
if(exportPrice != PRICE_NO_VALUE) { uint8_t exportPriceSensorNo = 0;
char path[20]; for(int pricePointIndex = currentPricePointIndex; pricePointIndex < numberOfPoints; pricePointIndex++) {
snprintf(path, 20, "exportprices['%d']", 0); float val = ps->getPricePoint(PRICE_DIRECTION_EXPORT, pricePointIndex);
HomeAssistantSensor sensor = { if(val == PRICE_NO_VALUE) break;
"Export price current hour", if(exportPriceSensorNo < priceExportInit) {
PriceSensor.topic, exportPriceSensorNo++;
path, continue;
PriceSensor.ttl, }
uom.c_str(),
PriceSensor.devcl, uint8_t resolution = ps->getResolutionInMinutes();
"total"
}; char path[64];
publishSensor(sensor); 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() { void HomeAssistantMqttHandler::publishSystemSensors() {
if(sInit) return; if(sInit) return;
for(uint8_t i = 0; i < SystemSensorCount; i++) { for(uint8_t i = 0; i < SystemSensorCount; i++) {
@@ -734,7 +789,8 @@ void HomeAssistantMqttHandler::publishThresholdSensors() {
RealtimeThresholdSensor.ttl, RealtimeThresholdSensor.ttl,
RealtimeThresholdSensor.uom, RealtimeThresholdSensor.uom,
RealtimeThresholdSensor.devcl, RealtimeThresholdSensor.devcl,
RealtimeThresholdSensor.stacl RealtimeThresholdSensor.stacl,
RealtimeThresholdSensor.uid
}; };
publishSensor(sensor); publishSensor(sensor);
} }
@@ -745,8 +801,49 @@ uint8_t HomeAssistantMqttHandler::getFormat() {
return 4; return 4;
} }
bool HomeAssistantMqttHandler::publishRaw(String data) { bool HomeAssistantMqttHandler::publishRaw(uint8_t* raw, size_t length) {
return false; 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) { void HomeAssistantMqttHandler::onMessage(String &topic, String &payload) {
@@ -756,9 +853,27 @@ void HomeAssistantMqttHandler::onMessage(String &topic, String &payload) {
if (debugger->isActive(RemoteDebug::INFO)) if (debugger->isActive(RemoteDebug::INFO))
#endif #endif
debugger->printf_P(PSTR("Received online status from HA, resetting sensor status\n")); 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 = false; 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 < 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

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -45,6 +45,7 @@ public:
bool applyBoardConfig(uint8_t boardType, GpioConfig& gpioConfig, MeterConfig& meterConfig, uint8_t hanPin); bool applyBoardConfig(uint8_t boardType, GpioConfig& gpioConfig, MeterConfig& meterConfig, uint8_t hanPin);
void setup(SystemConfig* sys, GpioConfig* gpio); void setup(SystemConfig* sys, GpioConfig* gpio);
float getVcc(); float getVcc();
void setMaxVcc(float maxVcc);
uint8_t getTempSensorCount(); uint8_t getTempSensorCount();
TempSensorData* getTempSensorData(uint8_t); TempSensorData* getTempSensorData(uint8_t);
bool updateTemperatures(); bool updateTemperatures();
@@ -67,7 +68,9 @@ private:
bool ledInvert, rgbInvert; bool ledInvert, rgbInvert;
uint8_t vccPin, vccGnd_r, vccVcc_r; uint8_t vccPin, vccGnd_r, vccVcc_r;
float vccOffset, vccMultiplier; float vccOffset, vccMultiplier;
float maxVcc = 3.2; // 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 float vcc = 3.3; // Last known Vcc
float maxVcc = 3.28; // 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; uint16_t analogRange = 1024;
AdcConfig voltAdc, tempAdc; AdcConfig voltAdc, tempAdc;

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -22,6 +22,9 @@ bool HwTools::applyBoardConfig(uint8_t boardType, GpioConfig& gpioConfig, MeterC
gpioConfig.vccResistorGnd = 22; gpioConfig.vccResistorGnd = 22;
gpioConfig.vccResistorVcc = 33; gpioConfig.vccResistorVcc = 33;
gpioConfig.ledDisablePin = 6; gpioConfig.ledDisablePin = 6;
gpioConfig.vccBootLimit = 0;
gpioConfig.vccOffset = 0;
gpioConfig.vccMultiplier = 0;
return true; return true;
case 51: // Wemos S2 mini case 51: // Wemos S2 mini
gpioConfig.ledPin = 15; gpioConfig.ledPin = 15;
@@ -201,15 +204,15 @@ void HwTools::setup(SystemConfig* sys, GpioConfig* config) {
pinMode(config->vccPin, INPUT); pinMode(config->vccPin, INPUT);
#endif #endif
vccPin = config->vccPin; vccPin = config->vccPin;
vccOffset = config->vccOffset / 100.0;
vccMultiplier = config->vccMultiplier / 1000.0;
vccGnd_r = config->vccResistorGnd;
vccVcc_r = config->vccResistorVcc;
} else { } else {
voltAdc.unit = 0xFF; voltAdc.unit = 0xFF;
voltAdc.channel = 0xFF; voltAdc.channel = 0xFF;
vccPin = 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) { if(config->tempAnalogSensorPin > 0 && config->tempAnalogSensorPin < 40) {
pinMode(config->tempAnalogSensorPin, INPUT); pinMode(config->tempAnalogSensorPin, INPUT);
@@ -416,18 +419,26 @@ float HwTools::getVcc() {
} }
volts = (x * 3.3) / 10.0 / analogRange; volts = (x * 3.3) / 10.0 / analogRange;
#endif #endif
} else { }
if(volts == 0.0) {
#if defined(ESP8266) #if defined(ESP8266)
volts = ESP.getVcc() / 1024.0; volts = ESP.getVcc() / 1024.0;
#else
return 0.0;
#endif #endif
} }
if(volts == 0.0) return 0.0;
if(vccGnd_r > 0 && vccVcc_r > 0) { if(vccPin != 0xFF) {
volts *= ((float) (vccGnd_r + vccVcc_r) / vccGnd_r); 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 vccOffset + (volts > 0.0 ? volts * vccMultiplier : 0.0); return volts;
} }
uint8_t HwTools::getTempSensorCount() { uint8_t HwTools::getTempSensorCount() {
@@ -648,10 +659,15 @@ bool HwTools::writeLedPin(uint8_t color, uint8_t state) {
} }
bool HwTools::isVoltageOptimal(float range) { bool HwTools::isVoltageOptimal(float range) {
if(boardType >= 5 && boardType <= 7 && maxVcc > 2.8) { // Pow-* if(boardType >= 1 && boardType <= 8 && maxVcc > 2.8) { // BUS-Power boards
float vcc = getVcc(); unsigned long now = millis();
if(now - lastVccRead > 250) {
vcc = getVcc();
lastVccRead = now;
}
if(vcc > 3.4 || vcc < 2.8) { if(vcc > 3.4 || vcc < 2.8) {
maxVcc = 0; // Voltage is outside the operating range, we have to assume voltage is OK maxVcc = 0;
return true; // Voltage is outside the operating range, we have to assume voltage is OK
} else if(vcc > maxVcc) { } else if(vcc > maxVcc) {
maxVcc = vcc; maxVcc = vcc;
} else { } else {
@@ -665,3 +681,7 @@ bool HwTools::isVoltageOptimal(float range) {
uint8_t HwTools::getBoardType() { uint8_t HwTools::getBoardType() {
return boardType; return boardType;
} }
void HwTools::setMaxVcc(float vcc) {
this->maxVcc = min(3.3f, vcc);
}

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -12,19 +12,19 @@
class JsonMqttHandler : public AmsMqttHandler { class JsonMqttHandler : public AmsMqttHandler {
public: public:
#if defined(AMS_REMOTE_DEBUG) #if defined(AMS_REMOTE_DEBUG)
JsonMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) { JsonMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, HwTools* hw, AmsDataStorage* ds, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
this->hw = hw;
};
#else #else
JsonMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) { JsonMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, HwTools* hw, AmsDataStorage* ds, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
this->hw = hw;
};
#endif #endif
this->hw = hw;
this->ds = ds;
};
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps); bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps);
bool publishTemperatures(AmsConfiguration*, HwTools*); bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*); bool publishPrices(PriceService*);
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea); 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); void onMessage(String &topic, String &payload);
@@ -32,6 +32,9 @@ public:
private: private:
HwTools* hw; HwTools* hw;
bool hasExport = false;
AmsDataStorage* ds;
uint16_t appendJsonHeader(AmsData* data); uint16_t appendJsonHeader(AmsData* data);
uint16_t appendJsonFooter(EnergyAccounting* ea, uint16_t pos); uint16_t appendJsonFooter(EnergyAccounting* ea, uint16_t pos);
bool publishList1(AmsData* data, EnergyAccounting* ea); bool publishList1(AmsData* data, EnergyAccounting* ea);
@@ -39,5 +42,6 @@ private:
bool publishList3(AmsData* data, EnergyAccounting* ea); bool publishList3(AmsData* data, EnergyAccounting* ea);
bool publishList4(AmsData* data, EnergyAccounting* ea); bool publishList4(AmsData* data, EnergyAccounting* ea);
String getMeterModel(AmsData* data); String getMeterModel(AmsData* data);
void toJsonIsoTimestamp(time_t t, char* buf, size_t buflen);
}; };
#endif #endif

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -8,12 +8,13 @@
#include "FirmwareVersion.h" #include "FirmwareVersion.h"
#include "hexutils.h" #include "hexutils.h"
#include "Uptime.h" #include "Uptime.h"
#include "AmsJsonGenerator.h"
bool JsonMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) { bool JsonMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
if(strlen(mqttConfig.publishTopic) == 0) { if(strlen(mqttConfig.publishTopic) == 0) {
return false; return false;
} }
if(!mqtt.connected()) { if(!connected()) {
return false; return false;
} }
@@ -44,6 +45,15 @@ bool JsonMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAcc
ret = publishList4(&data, ea); ret = publishList4(&data, ea);
mqtt.loop(); mqtt.loop();
} }
if(data.getListType() >= 2 && data.getActiveExportPower() > 0.0) {
hasExport = true;
}
if(data.getListType() >= 3 && data.getActiveExportCounter() > 0.0) {
hasExport = true;
}
loop(); loop();
return ret; return ret;
} }
@@ -68,13 +78,23 @@ uint16_t JsonMqttHandler::appendJsonFooter(EnergyAccounting* ea, uint16_t pos) {
memset(pf, 0, 4); memset(pf, 0, 4);
} }
return snprintf_P(json+pos, BufferSize-pos, PSTR("%s\"%sh\":%.2f,\"%sd\":%.1f,\"%st\":%d,\"%sx\":%.2f,\"%she\":%.2f,\"%sde\":%.1f%s"), 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\":%.3f,\"%sd\":%.2f,\"%sm\":%.1f,\"%st\":%d,\"%sx\":%.2f,\"%she\":%.3f,\"%sde\":%.2f,\"%sme\":%.1f,\"peaks\":[%s]%s"),
strlen(pf) == 0 ? "},\"realtime\":{" : ",", strlen(pf) == 0 ? "},\"realtime\":{" : ",",
pf, pf,
ea->getUseThisHour(), ea->getUseThisHour(),
pf, pf,
ea->getUseToday(), ea->getUseToday(),
pf, pf,
ea->getUseThisMonth(),
pf,
ea->getCurrentThreshold(), ea->getCurrentThreshold(),
pf, pf,
ea->getMonthMax(), ea->getMonthMax(),
@@ -82,6 +102,9 @@ uint16_t JsonMqttHandler::appendJsonFooter(EnergyAccounting* ea, uint16_t pos) {
ea->getProducedThisHour(), ea->getProducedThisHour(),
pf, pf,
ea->getProducedToday(), ea->getProducedToday(),
pf,
ea->getProducedThisMonth(),
peaks.c_str(),
strlen(pf) == 0 ? "}" : "" strlen(pf) == 0 ? "}" : ""
); );
} }
@@ -272,9 +295,9 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
} }
bool JsonMqttHandler::publishPrices(PriceService* ps) { bool JsonMqttHandler::publishPrices(PriceService* ps) {
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected()) if(strlen(mqttConfig.publishTopic) == 0 || !connected())
return false; return false;
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE) if(!ps->hasPrice())
return false; return false;
time_t now = time(nullptr); time_t now = time(nullptr);
@@ -285,7 +308,7 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
float values[38]; float values[38];
for(int i = 0;i < 38; i++) values[i] = PRICE_NO_VALUE; for(int i = 0;i < 38; i++) values[i] = PRICE_NO_VALUE;
for(uint8_t i = 0; i < 38; i++) { 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; values[i] = val;
if(val == PRICE_NO_VALUE) break; if(val == PRICE_NO_VALUE) break;
@@ -333,59 +356,89 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
memset(ts1hr, 0, 24); memset(ts1hr, 0, 24);
if(min1hrIdx > -1) { if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx); time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
tmElements_t tm; tmElements_t tm;
breakTime(ts, 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]; char ts3hr[24];
memset(ts3hr, 0, 24); memset(ts3hr, 0, 24);
if(min3hrIdx > -1) { if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx); time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
tmElements_t tm; tmElements_t tm;
breakTime(ts, 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]; char ts6hr[24];
memset(ts6hr, 0, 24); memset(ts6hr, 0, 24);
if(min6hrIdx > -1) { if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx); time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
tmElements_t tm; tmElements_t tm;
breakTime(ts, 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]; if(mqttConfig.payloadFormat == 6) {
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\","), WiFi.macAddress().c_str()); 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_"));
}
for(uint8_t i = 0;i < 38; i++) { for(uint8_t i = 0;i < 38; i++) {
if(values[i] == PRICE_NO_VALUE) { if(values[i] == PRICE_NO_VALUE) {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%s%d\":null,"), pf, i); pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_%d\":null,"), i);
} else { } else {
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%s%d\":%.4f,"), pf, i, values[i]); 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\"}"), pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_min\":%.4f,\"pr_max\":%.4f,\"pr_cheapest1hr\":%s,\"pr_cheapest3hr\":%s,\"pr_cheapest6hr\":%s}"),
pf, min == INT16_MAX ? 0.0 : min,
min == INT16_MAX ? 0.0 : min, max == INT16_MIN ? 0.0 : max,
pf, ts1hr,
max == INT16_MIN ? 0.0 : max, ts3hr,
pf, ts6hr
ts1hr, );
pf, } else {
ts3hr, uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{\"import\":["), WiFi.macAddress().c_str());
pf,
ts6hr uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex();
); uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
if(mqttConfig.payloadFormat != 6) { for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
json[pos++] = '}'; float val = ps->getPricePoint(PRICE_DIRECTION_IMPORT, i);
json[pos] = '\0'; 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; bool ret = false;
if(mqttConfig.payloadFormat == 5) { if(mqttConfig.payloadFormat == 5) {
@@ -400,7 +453,7 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
} }
bool JsonMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) { bool JsonMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) {
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected()) if(strlen(mqttConfig.publishTopic) == 0 || !connected())
return false; return false;
snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\"}"), snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\"}"),
@@ -428,9 +481,77 @@ uint8_t JsonMqttHandler::getFormat() {
return 0; return 0;
} }
bool JsonMqttHandler::publishRaw(String data) { bool JsonMqttHandler::publishRaw(uint8_t* raw, size_t length) {
return false; 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) { 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

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -11,6 +11,10 @@
#include "AmsConfiguration.h" #include "AmsConfiguration.h"
#include "DataParser.h" #include "DataParser.h"
#include "Cosem.h" #include "Cosem.h"
#include "Timezone.h"
#if defined(AMS_REMOTE_DEBUG)
#include "RemoteDebug.h"
#endif
#define NOVALUE 0xFFFFFFFF #define NOVALUE 0xFFFFFFFF
@@ -21,7 +25,11 @@ struct AmsOctetTimestamp {
class IEC6205675 : public AmsData { class IEC6205675 : public AmsData {
public: 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: private:
CosemData* getCosemDataAt(uint8_t index, const char* ptr); CosemData* getCosemDataAt(uint8_t index, const char* ptr);
@@ -30,8 +38,9 @@ private:
float getNumber(uint8_t* obis, int matchlength, const char* ptr); float getNumber(uint8_t* obis, int matchlength, const char* ptr);
float getNumber(CosemData*); float getNumber(CosemData*);
time_t getTimestamp(uint8_t* obis, int matchlength, const char* ptr); 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_UNKNOWN_1[4] = { 25, 9, 0, 255 };
uint8_t AMS_OBIS_VERSION[4] = { 0, 2, 129, 255 }; uint8_t AMS_OBIS_VERSION[4] = { 0, 2, 129, 255 };
uint8_t AMS_OBIS_METER_MODEL[4] = { 96, 1, 1, 255 }; uint8_t AMS_OBIS_METER_MODEL[4] = { 96, 1, 1, 255 };

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2024 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: All rights reserved * License: All rights reserved
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -12,7 +12,22 @@
#include "DataParser.h" #include "DataParser.h"
#include "Cosem.h" #include "Cosem.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; CosemBasic header;
CosemLongUnsigned u1; CosemLongUnsigned u1;
CosemLongUnsigned u2; CosemLongUnsigned u2;

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -13,6 +13,7 @@
#endif #endif
#include "AmsData.h" #include "AmsData.h"
#include "AmsConfiguration.h" #include "AmsConfiguration.h"
#include "AmsMqttHandler.h"
class MeterCommunicator { class MeterCommunicator {
public: public:
@@ -24,6 +25,13 @@ public:
virtual bool isConfigChanged(); virtual bool isConfigChanged();
virtual void ackConfigChanged(); virtual void ackConfigChanged();
virtual void getCurrentConfig(MeterConfig& meterConfig); virtual void getCurrentConfig(MeterConfig& meterConfig);
virtual void setMqttHandlerForDebugging(AmsMqttHandler* mqttHandler) {
this->mqttDebug = mqttHandler;
};
protected:
AmsMqttHandler* mqttDebug = NULL;
}; };
#endif #endif

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2023 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */
@@ -14,7 +14,7 @@
#include "AmsConfiguration.h" #include "AmsConfiguration.h"
#include "DataParsers.h" #include "DataParsers.h"
#include "Timezone.h" #include "Timezone.h"
#include "PassthroughMqttHandler.h" #include "AmsMqttHandler.h"
#if defined(ESP8266) #if defined(ESP8266)
#include "SoftwareSerial.h" #include "SoftwareSerial.h"
@@ -36,7 +36,9 @@ public:
bool isConfigChanged(); bool isConfigChanged();
void ackConfigChanged(); void ackConfigChanged();
void getCurrentConfig(MeterConfig& meterConfig); void getCurrentConfig(MeterConfig& meterConfig);
void setPassthroughMqttHandler(PassthroughMqttHandler*); void setTimezone(Timezone* tz) {
this->tz = tz;
};
HardwareSerial* getHwSerial(); HardwareSerial* getHwSerial();
void rxerr(int err); void rxerr(int err);
@@ -51,8 +53,6 @@ protected:
bool configChanged = false; bool configChanged = false;
Timezone* tz; Timezone* tz;
PassthroughMqttHandler* pt = NULL;
uint8_t *hanBuffer = NULL; uint8_t *hanBuffer = NULL;
uint16_t hanBufferSize = 0; uint16_t hanBufferSize = 0;
Stream *hanSerial; Stream *hanSerial;
@@ -62,11 +62,12 @@ protected:
HardwareSerial *hwSerial = NULL; HardwareSerial *hwSerial = NULL;
uint8_t rxBufferErrors = 0; uint8_t rxBufferErrors = 0;
bool autodetect = false, validDataReceived = false; bool autodetect = false;
uint8_t validDataReceived = 0;
unsigned long meterAutodetectLastChange = 0; unsigned long meterAutodetectLastChange = 0;
long rate = 10000; long rate = 10000;
uint32_t autodetectBaud = 0; uint32_t autodetectBaud = 0;
uint8_t autodetectParity = 11; uint8_t autodetectParity = 11; // 8E1
bool autodetectInvert = false; bool autodetectInvert = false;
uint8_t autodetectCount = 0; uint8_t autodetectCount = 0;
@@ -91,6 +92,7 @@ protected:
int16_t unwrapData(uint8_t *buf, DataParserContext &context); int16_t unwrapData(uint8_t *buf, DataParserContext &context);
void printHanReadError(int pos); void printHanReadError(int pos);
void handleAutodetect(unsigned long now); void handleAutodetect(unsigned long now);
uint8_t getNextParity(uint8_t parityOrdinal);
}; };
#endif #endif

View File

@@ -1,5 +1,5 @@
/** /**
* @copyright Utilitech AS 2024 * @copyright Utilitech AS 2023-2026
* License: Fair Source * License: Fair Source
* *
*/ */

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