Compare commits

...

190 Commits

Author SHA1 Message Date
Gunnar Skjold
7da617e8c2 Merge pull request #154 from gskjold/dev-1.6.0
v2.0
2021-12-10 08:52:10 +01:00
Gunnar Skjold
0b86761d2c Fixed a couple of things before release 2021-12-10 08:38:48 +01:00
Gunnar Skjold
d697f7e37f Final changes before 2.0 2021-12-10 08:19:48 +01:00
Gunnar Skjold
db859e3ff5 Fixed readme 2021-12-08 21:06:52 +01:00
Gunnar Skjold
94d22957bd Removed old screenshot 2021-12-08 21:06:00 +01:00
Gunnar Skjold
1ebaf443cc Updated readme 2021-12-08 21:05:42 +01:00
Gunnar Skjold
a336f711b0 Fixed day graph 2021-12-08 20:35:53 +01:00
Gunnar Skjold
e6d3b47d4f Some adjustment to data storage 2021-12-07 19:34:34 +01:00
Gunnar Skjold
eb479f8216 More adaptations for Austian meters 2021-12-07 19:07:43 +01:00
Gunnar Skjold
38cba4e8da Various fixes 2021-12-06 19:33:40 +01:00
Gunnar Skjold
bc6d45ecf2 Fixed WiFi instability? 2021-12-06 16:58:55 +01:00
Gunnar Skjold
097131d7fb Some changes 2021-12-06 10:47:42 +01:00
Gunnar Skjold
2107ca50e4 More changes for Austrian meters, untested 2021-12-06 10:47:08 +01:00
Gunnar Skjold
178f603937 Fix 2021-12-05 20:14:46 +01:00
Gunnar Skjold
21687368c6 Preparing for austrian meters 2021-12-05 20:09:34 +01:00
Gunnar Skjold
6054e900e6 Experimenting 2021-12-05 13:08:34 +01:00
Gunnar Skjold
3da5275624 Fixed merge problem 2021-12-05 12:21:21 +01:00
Gunnar Skjold
3fda2cfe7a Removed test code 2021-12-05 12:16:13 +01:00
Gunnar Skjold
2bb651f95c Merge branch 'master' into dev-1.6.0 2021-12-05 12:15:39 +01:00
Gunnar Skjold
e2442a26ee Trying to figure out esp8266 wifi reconnect 2021-12-05 12:07:34 +01:00
Gunnar Skjold
cbd2ab4a7a Trying to figure out esp8266 wifi reconnect 2021-12-05 12:07:20 +01:00
Gunnar Skjold
76f8e2c343 2.0 development 2021-12-04 15:41:13 +01:00
Gunnar Skjold
ab101c8622 Some changes after testing 2021-11-30 08:01:20 +01:00
Gunnar Skjold
f425abb52d Added missing file from last commit 2021-11-27 20:29:18 +01:00
Gunnar Skjold
dc5bfff655 Merge branch 'master' into dev-1.6.0 2021-11-27 20:23:35 +01:00
Gunnar Skjold
eb59245118 Added usage plots and ADC reading for Vcc 2021-11-27 20:16:26 +01:00
Gunnar Skjold
d18fd27a24 Fixed accumulated values from Kaifa 1p 2021-11-23 20:55:55 +01:00
Gunnar Skjold
6f09f523e4 Added possibility to upgrade from GUI if there is enough RAM for SSL 2021-11-18 20:42:13 +01:00
Gunnar Skjold
e78d59c31a Different menu for ESP8266 and ESP32 2021-11-18 20:28:29 +01:00
Gunnar Skjold
e7f3217d7b Added support for power factor 2021-11-18 20:20:51 +01:00
Gunnar Skjold
ab534ce60a Ignore firmware update if AP button is pressed 2021-11-18 19:43:48 +01:00
Gunnar Skjold
933246eae8 Changed from SPIFFS to LittleFS 2021-11-18 19:40:20 +01:00
Gunnar Skjold
5c38d1cf3e Fixes after testing 2021-11-18 19:06:33 +01:00
Gunnar Skjold
580085f717 Fixed kamstrup list version 2021-11-17 21:47:00 +01:00
Gunnar Skjold
9831d4aa78 Removed buffer size 2021-11-17 21:46:37 +01:00
Gunnar Skjold
a3561d5c58 Fixed free mem formatting bug 2021-11-17 20:50:30 +01:00
Gunnar Skjold
2a524cd0ac Fixed Kamstrup bug 2021-11-17 20:42:45 +01:00
Gunnar Skjold
254e010594 Fixed save crash 2021-11-16 20:40:21 +01:00
Gunnar Skjold
24025d6785 Added support for IEC62056-21 2021-11-16 20:07:47 +01:00
Gunnar Skjold
4c92e592d6 Added support for uart_swap for ESP8266 2021-11-08 12:05:48 +01:00
Gunnar Skjold
f192ddae81 Some changes while testing 2021-11-06 21:02:38 +01:00
Gunnar Skjold
1cd2446365 Some modifications to support timestamp from all meters 2021-11-06 19:44:50 +01:00
Gunnar Skjold
6d26102b8e New meter config 2021-11-06 19:30:58 +01:00
Gunnar Skjold
8e9da8f255 First step in implementing a new DLMS parser 2021-11-06 16:56:02 +01:00
Gunnar Skjold
72bdb6e363 Update issue templates 2021-11-01 07:14:54 +01:00
Gunnar Skjold
6df942f488 Revert time version update 2021-10-31 09:37:58 +01:00
Gunnar Skjold
1e323ac3b9 Various changes for version updates and some debugging for upload 2021-10-31 09:33:25 +01:00
Gunnar Skjold
05bdbaf1f5 Fixed error 2021-10-30 18:42:59 +02:00
Gunnar Skjold
6cca25788e Fixed copy/paste error 2021-10-23 10:45:57 +02:00
Gunnar Skjold
e929f87ea9 Fixed decimals in json 2021-10-23 10:38:53 +02:00
Gunnar Skjold
21a4102553 Changes for correct time conversion 2021-10-23 10:05:14 +02:00
Gunnar Skjold
768645fc0a Added more decimals in data.json 2021-10-23 10:04:52 +02:00
Gunnar Skjold
c670549dea Warning for BUS powered devices on firmware upgrade 2021-10-23 09:21:53 +02:00
Gunnar Skjold
147b2ca33e Fixed build error 2021-10-23 09:07:07 +02:00
Gunnar Skjold
c645b82ed3 Fixed voltage and amp meters 2021-10-23 09:04:00 +02:00
Gunnar Skjold
26bb8a0fea Support two phase power calculation 2021-10-23 08:59:16 +02:00
Gunnar Skjold
507ed13770 Added GPIO designation to UART in dropdown 2021-10-23 08:40:40 +02:00
Gunnar Skjold
a0f53a0c52 Partial fix of dead gauges 2021-10-21 21:17:03 +02:00
Gunnar Skjold
13bbc81b7f Clear debug config on setup if not already set 2021-09-23 20:56:24 +02:00
Gunnar Skjold
7412ba2697 Removed temp sensor update if no sensors were found 2021-09-23 20:30:22 +02:00
Gunnar Skjold
dce7b7e64b Support for long MQTT username and password 2021-09-23 19:28:55 +02:00
Gunnar Skjold
7627d6c369 Updated readme 2021-09-23 18:48:58 +02:00
Gunnar Skjold
9fc9adea1c Fixed crash when enabling substitute values in meter config 2021-09-23 18:30:15 +02:00
Gunnar Skjold
46df07fe40 Fixed build error on ESP32 2021-09-23 18:20:54 +02:00
Gunnar Skjold
0aeb5555e7 Fixed long web passwords 2021-09-23 18:10:16 +02:00
Gunnar Skjold
181fe3c909 Removed meter from setup 2021-09-23 17:36:43 +02:00
Gunnar Skjold
867d6a4ef8 Added profiles for Pow board 2021-09-23 17:34:21 +02:00
Gunnar Skjold
d52216a73e Moved hardware page to Wiki 2021-09-23 17:28:51 +02:00
Gunnar Skjold
d70b41a454 Added images for Pow 2021-09-23 17:25:52 +02:00
Gunnar Skjold
332c366561 Renamed kamstrup meters 2021-09-22 11:44:59 +02:00
Gunnar Skjold
b572ad97f8 Added Kamstrup document 2021-09-22 11:42:22 +02:00
Gunnar Skjold
aa307f8690 Fixed incorrect value for accumulated export 2021-09-22 11:40:48 +02:00
Gunnar Skjold
19223312b5 Fixed danish KAmstrup 2021-09-13 10:14:47 +02:00
Gunnar Skjold
74bc5aa7a0 Fixed build 2021-09-13 10:14:28 +02:00
Gunnar Skjold
e66e8a96ff Added some frames as documentation 2021-09-13 08:10:39 +02:00
Gunnar Skjold
ce95360a64 Minor fix 2021-09-13 08:08:39 +02:00
Gunnar Skjold
7802aeaab1 Removed unnecessary config for arduinojson 2021-09-13 08:07:38 +02:00
Gunnar Skjold
37ce3566bf Fixed config clear 2021-09-13 08:07:03 +02:00
Gunnar Skjold
606bac100a Cleanup 2021-03-24 09:37:46 +01:00
Gunnar Skjold
be569d1802 Fixed buffer for JSON generation 2021-03-17 20:36:40 +01:00
Gunnar Skjold
3e21105b2d Fixed prices in JSON handler 2021-03-17 20:33:11 +01:00
Gunnar Skjold
9946827431 Fixed cheapest price calculation 2021-03-17 20:17:36 +01:00
Gunnar Skjold
90d03ca77f Retain flag for prices 2021-03-16 11:44:01 +01:00
Gunnar Skjold
7727b17122 Fixed publishing of prices 2021-03-16 11:02:59 +01:00
Gunnar Skjold
fbc4c8c502 Merge pull request #101 from gskjold/dev-v1.5.0
Dev v1.5.0
2021-03-10 19:22:23 +01:00
Gunnar Skjold
5d6a4ea0a4 Updated readme 2021-03-10 19:15:00 +01:00
Gunnar Skjold
91ce486bde Updated readme 2021-03-10 19:14:22 +01:00
Gunnar Skjold
523139749e Auto install minifier 2021-03-10 19:14:12 +01:00
Gunnar Skjold
5b9f3b7aed Minor changes after testing 2021-03-10 18:33:45 +01:00
Gunnar Skjold
af76243761 Retain flag for accumulated values 2021-03-10 17:45:16 +01:00
Gunnar Skjold
605dc4901c Fixed incorrect check for 32/35 A main fuse 2021-03-10 17:45:02 +01:00
Gunnar Skjold
54fb950513 Changes after testing 2021-01-22 08:01:23 +01:00
Gunnar Skjold
ff84278edf Some changes after testing 2021-01-20 07:41:09 +01:00
Gunnar Skjold
f15cf5d75e Some changes after testing 2021-01-18 20:32:51 +01:00
Gunnar Skjold
33070af111 Refactored MQTT payload handling into separate classes 2021-01-17 20:11:04 +01:00
Gunnar Skjold
53573184f3 Simplified temperature.json 2021-01-17 15:53:56 +01:00
Gunnar Skjold
decc4788a7 Simplified code that generates data.json 2021-01-17 15:08:01 +01:00
Gunnar Skjold
f9597c786e Changed to BearSSL for meter decryption on ESP8266 + some minor changes 2021-01-17 12:34:57 +01:00
Gunnar Skjold
af8f5a7c24 Memory optimization and bugfix 2021-01-16 16:02:39 +01:00
Gunnar Skjold
a830a52863 Stripped HTML to improve stability 2021-01-14 20:03:49 +01:00
Gunnar Skjold
037bac24de Changes in user interface 2021-01-14 16:19:00 +01:00
Gunnar Skjold
837c3cf802 Taking timezone into account when calculating midnight 2021-01-11 20:21:36 +01:00
Gunnar Skjold
75956c087c Closing http connections after receiving data 2021-01-11 09:49:25 +01:00
Gunnar Skjold
88528b4099 Added reboot and cleaned up a bit in UI 2021-01-10 22:30:55 +01:00
Gunnar Skjold
6176d34e84 Link to documentation 2021-01-10 22:03:02 +01:00
Gunnar Skjold
f2dda26bbb Added support for retrieving energy price from ENTSO-E API 2021-01-10 20:54:25 +01:00
Gunnar Skjold
402ecf67d7 Merge branch 'master' into dev-v1.5.0 2021-01-09 11:25:31 +01:00
Gunnar Skjold
a83a6c1c53 Fixed release 2021-01-09 11:21:01 +01:00
Gunnar Skjold
5beb13894c Initial implementation of supporting timezone in timestamp 2021-01-09 11:11:49 +01:00
Gunnar Skjold
c3fa618ab2 Fixed build problem 2021-01-09 09:50:20 +01:00
Gunnar Skjold
7713ae8566 Trying to fix GitHub Actions build issue 2021-01-09 09:36:20 +01:00
Gunnar Skjold
7ae860ec72 Fixed incorrect reading of analog temperature 2021-01-09 09:33:09 +01:00
Gunnar Skjold
376008a735 Merge pull request #96 from kallemooo/fixFromHex
Changed fromHex() to use an supplied buffer
2021-01-04 09:58:32 +01:00
Karl Thorén
feed10184b Changed fromHex() to use an supplied buffer
Solves the problem with returning a pointer to local variable.

Signed-off-by: Karl Thorén <karl.h.thoren@gmail.com>
2020-12-27 16:29:53 +01:00
Gunnar Skjold
59ca29f6a8 Update README.md 2020-09-25 06:46:16 +02:00
Gunnar Skjold
644a3fa40b Fixed factory reset 2020-09-05 20:03:06 +02:00
Gunnar Skjold
a2c1c8fc61 Updated ESP32 flash procedure 2020-09-05 19:27:36 +02:00
Gunnar Skjold
b823f029ea Updated readme 2020-09-05 19:12:59 +02:00
Gunnar Skjold
fd907deec1 Merge pull request #87 from gskjold/dev-v1.4.0
Dev v1.4.0
2020-09-05 19:05:01 +02:00
Gunnar Skjold
c1b56e25ad Minor changes 2020-09-05 18:59:52 +02:00
Gunnar Skjold
5491088cec Cleared out some unnecessary files from mbedtls lib 2020-09-05 11:14:00 +02:00
Gunnar Skjold
04271accc1 Dynamic temperature page 2020-09-05 11:10:36 +02:00
Gunnar Skjold
a332e38b97 Some cleanup 2020-09-05 09:40:07 +02:00
Gunnar Skjold
17bd85ebd0 Added more debugging code. Also some changes after testing 2020-09-04 07:25:11 +02:00
Gunnar Skjold
4207216770 Some changes for mbedtls when building ESP32 2020-08-28 21:20:11 +02:00
Gunnar Skjold
02491e074b Added comment 2020-08-28 20:33:46 +02:00
Gunnar Skjold
e79a0585f0 Some changes during testing 2020-08-28 19:58:33 +02:00
Gunnar Skjold
859220d33f Added missing getter 2020-08-28 17:55:07 +02:00
Gunnar Skjold
5d47105951 Added support for TMP236 analog temp sensor 2020-08-28 17:46:34 +02:00
Gunnar Skjold
e71f937856 Some changes during testing 2020-08-06 13:11:31 +02:00
Gunnar Skjold
d730aac33b Send temperature updates to MQTT 2020-08-05 20:33:49 +02:00
Gunnar Skjold
e121ec75d8 Support multiple temperature sensors 2020-08-05 19:55:16 +02:00
Gunnar Skjold
6479fd6a63 Splitted system config into Web, NTP, GPIO and Debugging 2020-08-05 13:45:10 +02:00
Gunnar Skjold
603f2925ce Removing mbedtls code in HanReader lib 2020-07-31 11:25:45 +02:00
Gunnar Skjold
1284f3f848 Using internal mbedtls lib for encrypted meters. Next up, find out how to modify config.h when using external lib. 2020-07-31 11:19:23 +02:00
Gunnar Skjold
620e355a27 Support encrypted meters and added vcc offset 2020-07-31 10:15:11 +02:00
Gunnar Skjold
47ddf57547 Merge branch 'master' into dev-v1.4.0 2020-07-26 11:24:43 +02:00
Gunnar Skjold
fcbfe4d96f Changes to make substituted I2 correct when exporting power 2020-07-25 09:17:39 +02:00
Gunnar Skjold
d789d6ff3b Reduced number of build targets 2020-07-25 09:15:05 +02:00
Gunnar Skjold
b4f18de030 Made base64 work for both platforms 2020-07-25 08:48:51 +02:00
Gunnar Skjold
00d5d215cd Switched to internal base64 2020-07-25 08:45:31 +02:00
Gunnar Skjold
8de5a58a6b Changed dependency for Base64 2020-07-06 19:17:45 +02:00
Gunnar Skjold
012794e682 Make web content minifier optional 2020-07-06 19:14:37 +02:00
Gunnar Skjold
38eb2d8c19 Merge pull request #79 from gskjold/dev-v1.3.0
Dev v1.3.0
2020-06-06 20:30:47 +02:00
Gunnar Skjold
d95137adbc Support for Kamstrup IT meters 2020-05-28 18:44:38 +02:00
Gunnar Skjold
cde3f80fca Ensure that GPIO is correctly set after setup 2020-05-28 14:19:03 +02:00
Gunnar Skjold
cdc012743f Changes for building on GitHub actions for all branches 2020-05-27 08:38:59 +02:00
Gunnar Skjold
23c90315a5 Some changes before public testing 2020-05-24 17:14:16 +02:00
Gunnar Skjold
8d938f111c Fixed mDNS 2020-05-24 16:30:51 +02:00
Gunnar Skjold
cff6c02d57 Some cleanup and changes. Trying to get self-update to work, no luck 2020-05-22 22:26:31 +02:00
Gunnar Skjold
0c92f7401c Removed unused code 2020-05-22 17:24:17 +02:00
Gunnar Skjold
195a0d4e77 Merge branch 'dev-v1.3.0' of github.com:gskjold/AmsToMqttBridge into dev-v1.3.0 2020-05-22 17:23:41 +02:00
Gunnar Skjold
43f50e0e0a Check for updates from GitHub and minor changes during testing 2020-05-22 17:23:38 +02:00
Gunnar Skjold
f2f20afd9c Merge pull request #76 from atlej68/dev-1.3.0-test
dev-1.3.0-testing: Timezone for esp32 and minor domoticz fix.
2020-05-22 17:22:54 +02:00
Atle Johansen
f22cfbb223 Domoticz: make config save values, cleanup. 2020-05-16 18:39:23 +02:00
Atle Johansen
803c5116bd alt. to Tz.h for esp32 (Tz.h not avail. for esp32) 2020-05-16 17:04:28 +02:00
Gunnar Skjold
a542fbc931 Yellow flash for AP mode if RGB LED is configured 2020-05-16 10:20:27 +02:00
Gunnar Skjold
85a70016fa Checking for new version on GitHub 2020-05-14 21:13:13 +02:00
Gunnar Skjold
778daf8645 Fixed som HTML and JS problems. Removed external NTP library and using espressif internal NTP client instead 2020-05-10 12:29:00 +02:00
Gunnar Skjold
efa99f970c Factory reset and splitted HTML into head, content and footer files to reduce memory footprint while parsing HTML templates 2020-05-10 11:51:00 +02:00
Gunnar Skjold
953f2d4110 Some modifications to increase stability 2020-05-08 22:58:16 +02:00
Gunnar Skjold
c3c0ca0a1b Added option to substitute missing I2 for Aidon IT meters. Also cleaned up some more UI 2020-05-04 16:07:23 +02:00
Gunnar Skjold
dc83853d2e Fixed some GPIO and Vcc configuration issues 2020-05-03 21:27:19 +02:00
Gunnar Skjold
f5123e9aa1 UI cleanup and added devkit board to initial setup 2020-05-03 21:01:58 +02:00
Gunnar Skjold
2da69dd451 Merge branch 'dev-v1.3.0' of github.com:gskjold/AmsToMqttBridge into dev-v1.3.0 2020-05-03 16:49:28 +02:00
Gunnar Skjold
2858123c1b Added configuration of GPIO in UI. Added initial setup page in AP mode. Major changes in storing configuration. 2020-05-03 16:29:38 +02:00
Gunnar Skjold
9d4488f8df Merge pull request #70 from atlej68/add_domoticz
Add domoticz
2020-05-03 16:28:05 +02:00
Gunnar Skjold
f78c8e3582 Merge pull request #69 from atlej68/add_devkit
Add devkit boards
2020-05-03 16:27:45 +02:00
Atle Johansen
431d6714b5 fix typo 2020-05-01 15:46:16 +02:00
Atle Johansen
550a3c1a0b reverse local change 2020-05-01 15:24:17 +02:00
Atle Johansen
b6f5e72638 merge with 1..3.0 and implementation of comments 2020-05-01 15:22:39 +02:00
Atle Johansen
958ff37d7d Merge remote-tracking branch 'upstream/dev-v1.3.0' into add_domoticz 2020-05-01 14:21:55 +02:00
Gunnar Skjold
1ea9da22c7 Merge pull request #68 from stenjo/feature/fullreport
Implementation of MQTT RAW full format
2020-05-01 12:16:06 +02:00
Gunnar Skjold
0c93c52e3d Implemented support for MQTT SSL and cleaned up necessary code 2020-05-01 12:10:08 +02:00
Atle Johansen
41784511e9 Add config for direct MQTT messages to Domoticz 2020-04-30 18:56:21 +02:00
Atle Johansen
a78fdc0b59 Add devkit boards 2020-04-30 18:28:12 +02:00
Gunnar Skjold
cc032fdf29 Implemented upload of certificates for MQTT SSL 2020-04-29 21:04:42 +02:00
Sten Otto Johnsen
f696e0b59b Removed checksum change 2020-04-24 12:15:25 +02:00
Sten Otto Johnsen
f67f12a188 Adding full report on raw data 2020-04-24 00:05:12 +02:00
Gunnar Skjold
719ed56e21 Merge pull request #66 from gskjold/dev-v1.2.3
Dev v1.2.3
2020-04-19 20:23:05 +02:00
Gunnar Skjold
cc72d0e0b3 Uncommited changes 2020-04-18 19:18:39 +02:00
Gunnar Skjold
ab175ec9ec Added correct conversion for int8, int16 and uint32 2020-04-18 11:04:43 +02:00
Gunnar Skjold
398407350c Fixed typ-o in MQTT topic for temperature and moved voltage bootup check into compiler option SELF_POWERED 2020-04-10 08:52:56 +02:00
Gunnar Skjold
5e33a15e85 Fixed build file 2020-04-09 08:58:56 +02:00
Gunnar Skjold
7f51534e91 Fixed page title on reboot page 2020-04-09 08:56:50 +02:00
Gunnar Skjold
0f5af6b274 Fixed ESP32 crash with RemoteDebug and unformatted SPIFFS. Added battery voltage for Lolin D32 and moved ESP_VCC_CALIB_FACTOR to cover all boards 2020-04-09 08:44:25 +02:00
Gunnar Skjold
7886ce668e Fixed build file? 2020-04-07 21:16:31 +02:00
Gunnar Skjold
0f4848c872 Added missing dep 2020-04-07 20:56:49 +02:00
Gunnar Skjold
6911d203ca Merge pull request #56 from gskjold/dev-v1.2
Version 1.2
2020-04-07 20:53:39 +02:00
Gunnar Skjold
e96b5bbf1b Changes during testing 2020-03-30 21:14:58 +02:00
127 changed files with 10201 additions and 5192 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Report a bug
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Hardware information:**
- Meter: [e.g. Aidon]
- AMS reader: [e.g. Pow-U, ESP32 etc]
- M-bus adapter (if applicable):
**Relevant firmware information:**
- Version: [e.g. 1.5.0]
- MQTT: [yes/no]
- HAN GPIO: [e.g. GPIO5]
- Temperature sensors [e.g. 3xDS18B20]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

22
.github/ISSUE_TEMPLATE/support.md vendored Normal file
View File

@@ -0,0 +1,22 @@
---
name: Support
about: Request support
title: ''
labels: ''
assignees: ''
---
**Describe your problem**
A clear and concise description of what the problem is.
**Hardware information:**
- Meter: [e.g. Aidon]
- AMS reader: [e.g. Pow-U, ESP32 etc]
- M-bus adapter (if applicable):
**Relevant firmware information:**
- Version: [e.g. 1.5.0]
- MQTT: [yes/no]
- HAN GPIO: [e.g. GPIO5]
- Temperature sensors [e.g. 3xDS18B20]

View File

@@ -9,7 +9,7 @@ on:
- web/**
- platformio.ini
branches:
- master
- '*'
tags:
- '*'
- '!v*.*.*'
@@ -39,9 +39,9 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -U platformio
pip install -U platformio css_html_js_minify
- name: Configure build targets
run: echo "[platformio]\ndefault_envs = hw1esp12e, esp12e, esp32" > platformio-user.ini
run: echo "[platformio]\ndefault_envs = esp8266, esp32" > platformio-user.ini
- name: PlatformIO lib install
run: pio lib install
- name: PlatformIO run

View File

@@ -20,10 +20,9 @@ jobs:
GITHUB_REF: ${{ github.ref }}
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11})
- name: Get release version for code
id: release_tag
env:
GITHUB_REF: ${{ github.ref }}
run: echo ::set-env name=GITHUB_TAG::$(echo ${GITHUB_REF##*/})
run: echo "GITHUB_TAG=$(echo ${GITHUB_REF##*/})" >> $GITHUB_ENV
- name: Cache Python dependencies
uses: actions/cache@v1
with:
@@ -41,7 +40,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -U platformio
pip install -U platformio css_html_js_minify
- name: PlatformIO lib install
run: pio lib install
- name: PlatformIO run
@@ -56,32 +55,14 @@ jobs:
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: Upload hw1esp12e binary to release
- name: Upload esp8266 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/hw1esp12e/firmware.bin
asset_name: ams2mqtt-hw1esp12e-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp12e 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/esp12e/firmware.bin
asset_name: ams2mqtt-esp12e-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload d1mini 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/d1mini/firmware.bin
asset_name: ams2mqtt-d1mini-${{ steps.release_tag.outputs.tag }}.bin
asset_path: .pio/build/esp8266/firmware.bin
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32 binary to release
uses: actions/upload-release-asset@v1
@@ -92,21 +73,12 @@ jobs:
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 lolind32 binary to release
- name: Upload esp32 partitions 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/lolind32/firmware.bin
asset_name: ams2mqtt-lolind32-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload featheresp32 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/featheresp32/firmware.bin
asset_name: ams2mqtt-featheresp32-${{ steps.release_tag.outputs.tag }}.bin
asset_path: .pio/build/esp32/partitions.bin
asset_name: ams2mqtt-esp32-partitions-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream

3
.gitignore vendored
View File

@@ -10,3 +10,6 @@ platformio-user.ini
/src/version.h
/src/web/root
/src/AmsToMqttBridge.ino.cpp
/test
/web/test.html
/sdkconfig

View File

@@ -1,34 +1,12 @@
# AMS <-> MQTT Bridge
Orignally designed and coded by [@roarfred](https://github.com/roarfred), see the original repo at [roarfred/AmsToMqttBridge](https://github.com/roarfred/AmsToMqttBridge)
# AMS MQTT Bridge
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 repository contains the code and schematics necessary to build a device to receive and convert data from AMS electrical meters installed in Norway. The code can be used on both ESP8266 and ESP32, both as custom build devices or built from readily available development modules. It reads data from the HAN port of the meter and sends this to a configured MQTT bus.
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/gskjold/AmsToMqttBridge/wiki). If you don't have the knowledge to set up a ESP device yourself, have a look at the shop at [amsleser.no](https://amsleser.no/).
There is also a web interface available on runtime, showing meter data in real time.
<img src="webui.jpg" width="480">
## Hardware options
Look in [hardware section](/hardware) for more details about supported hardware
## Release binaries
In the [Release section](https://github.com/gskjold/AmsToMqttBridge/releases) of this repository, you will find precompiled binaries for some common boards.
- _hw1esp12e_ :: First version custom hardware with ESP 12E of 12F chip
- _esp12e_ :: General ESP8266 board with 12E or 12F chip
- _d1mini_ :: Wemos D1 mini
- _esp32_ :: General ESP32 board
- _lolind32_ :: Wemos D32
- _featheresp32_ :: Adafruit ESP32 feather
### Flashing binaries with [esptool.py](https://github.com/espressif/esptool)
Linux:
```esptool.py --port /dev/ttyUSB0 write_flash 0x0 binary-file.bin```
Windows:
```esptool.py --port COM1 write_flash 0x0 binary-file.bin```
<img src="webui.png">
Go to the [WiKi](https://github.com/gskjold/AmsToMqttBridge/wiki) for information on how to get your own device! And find the latest prebuilt firmware file at the [release section](https://github.com/gskjold/AmsToMqttBridge/releases).
## Building this project with PlatformIO
To build this project, you need [PlatformIO](https://platformio.org/) installed.
@@ -39,4 +17,4 @@ It is recommended to use Visual Studio Code with the PlatformIO plugin for devel
[PlatformIO vscode plugin](https://platformio.org/install/ide?install=vscode)
Copy the ```platformio-user.ini-example``` to ```platformio-user.ini``` and customize to your preference. The code will adapt to the platform and board set in your profile. If you are using the original board design by [@roarfred](https://github.com/roarfred) use build flag -D HW_ROARFRED=1
For development purposes, copy the ```platformio-user.ini-example``` to ```platformio-user.ini``` and customize to your preference. The code will adapt to the platform and board set in your profile.

BIN
doc/Aidon-RJ12.pdf Normal file

Binary file not shown.

18
doc/Aidon_OBIS.txt Normal file
View File

@@ -0,0 +1,18 @@
1.1.0.2.129.255 - List version identifier
0.0.96.1.0.255 - Meter ID
0.0.96.1.7.255 - Meter Model
1.0.1.7.0.255 - Active+ Instantaneous value
1.0.2.7.0.255 - Active- Instantaneous value
1.0.3.7.0.255 - Reactive+ Instantaneous value
1.0.4.7.0.255 - Reactive- Instantaneous value
1.0.31.7.0.255 - L1 Current Instantaneous value
1.0.51.7.0.255 - L2 Current Instantaneous value
1.0.71.7.0.255 - L3 Current Instantaneous value
1.0.32.7.0.255 - L1 Voltage Instantaneous value
1.0.52.7.0.255 - L2 Voltage Instantaneous value
1.0.72.7.0.255 - L3 Voltage Instantaneous value
0.0.1.0.0.255 - Current date/time
1.0.1.8.0.255 - Active+ Energy
1.0.2.8.0.255 - Active- Energy
1.0.3.8.0.255 - Reactive+ Energy
1.0.4.8.0.255 - Reactive- Energy

119
doc/Aidon_data.xml Normal file
View File

@@ -0,0 +1,119 @@
<GatewayRequest>
<NetworkId Value="231" />
<PhysicalDeviceAddress Value="" />
<DataNotification>
<LongInvokeIdAndPriority Value="40000000" />
<DateTime Value="" />
<NotificationBody>
<DataValue>
<Array Qty="0D" >
<Structure Qty="02" >
<!--1.1.0.2.129.255-->
<OctetString Value="0101000281FF" />
<String Value="AIDON_V0001" />
</Structure>
<Structure Qty="02" >
<!--0.0.96.1.0.255-->
<OctetString Value="0000600100FF" />
<String Value="0000000000000000" />
</Structure>
<Structure Qty="02" >
<!--0.0.96.1.7.255-->
<OctetString Value="0000600107FF" />
<String Value="6534" />
</Structure>
<Structure Qty="03" >
<!--1.0.1.7.0.255-->
<OctetString Value="0100010700FF" />
<UInt32 Value="00000339" />
<Structure Qty="02" >
<Int8 Value="00" />
<Enum Value="1B" />
</Structure>
</Structure>
<Structure Qty="03" >
<!--1.0.2.7.0.255-->
<OctetString Value="0100020700FF" />
<UInt32 Value="00000000" />
<Structure Qty="02" >
<Int8 Value="00" />
<Enum Value="1B" />
</Structure>
</Structure>
<Structure Qty="03" >
<!--1.0.3.7.0.255-->
<OctetString Value="0100030700FF" />
<UInt32 Value="00000000" />
<Structure Qty="02" >
<Int8 Value="00" />
<Enum Value="1D" />
</Structure>
</Structure>
<Structure Qty="03" >
<!--1.0.4.7.0.255-->
<OctetString Value="0100040700FF" />
<UInt32 Value="00000251" />
<Structure Qty="02" >
<Int8 Value="00" />
<Enum Value="1D" />
</Structure>
</Structure>
<Structure Qty="03" >
<!--1.0.31.7.0.255-->
<OctetString Value="01001F0700FF" />
<Int16 Value="0012" />
<Structure Qty="02" >
<Int8 Value="FF" />
<Enum Value="21" />
</Structure>
</Structure>
<Structure Qty="03" >
<!--1.0.51.7.0.255-->
<OctetString Value="0100330700FF" />
<Int16 Value="0003" />
<Structure Qty="02" >
<Int8 Value="FF" />
<Enum Value="21" />
</Structure>
</Structure>
<Structure Qty="03" >
<!--1.0.71.7.0.255-->
<OctetString Value="0100470700FF" />
<Int16 Value="0016" />
<Structure Qty="02" >
<Int8 Value="FF" />
<Enum Value="21" />
</Structure>
</Structure>
<Structure Qty="03" >
<!--1.0.32.7.0.255-->
<OctetString Value="0100200700FF" />
<UInt16 Value="08FE" />
<Structure Qty="02" >
<Int8 Value="FF" />
<Enum Value="23" />
</Structure>
</Structure>
<Structure Qty="03" >
<!--1.0.52.7.0.255-->
<OctetString Value="0100340700FF" />
<UInt16 Value="08F8" />
<Structure Qty="02" >
<Int8 Value="FF" />
<Enum Value="23" />
</Structure>
</Structure>
<Structure Qty="03" >
<!--1.0.72.7.0.255-->
<OctetString Value="0100480700FF" />
<UInt16 Value="08F7" />
<Structure Qty="02" >
<Int8 Value="FF" />
<Enum Value="23" />
</Structure>
</Structure>
</Array>
</DataValue>
</NotificationBody>
</DataNotification>
</GatewayRequest>

View File

@@ -0,0 +1,32 @@
1.1.1.8.0.255 - Active+ Energy
1.1.2.8.0.255 - Active- Energy
1.1.3.8.0.255 - Reactive+ Energy
1.1.4.8.0.255 - Reactive- Energy
1.1.0.0.1.255 - Electricity ID?
1.1.1.7.0.255 - Active+ Instantaneous value
1.1.2.7.0.255 - Active- Instantaneous value
1.1.3.7.0.255 - Reactive+ Instantaneous value
1.1.4.7.0.255 - Reactive- Instantaneous value
0.1.1.0.0.255 - Current date/time
1.1.32.7.0.255 - L1 Voltage Instantaneous value
1.1.52.7.0.255 - L2 Voltage Instantaneous value
1.1.72.7.0.255 - L3 Voltage Instantaneous value
1.1.31.7.0.255 - L1 Current Instantaneous value
1.1.51.7.0.255 - L2 Current Instantaneous value
1.1.71.7.0.255 - L3 Current Instantaneous value
1.1.21.7.0.255 - L1 Active+ Instantaneous value
1.1.41.7.0.255 - L2 Active+ Instantaneous value
1.1.61.7.0.255 - L3 Active+ Instantaneous value
1.1.33.7.0.255 - L1 (cos.phi) (PF) Instantaneous value
1.1.53.7.0.255 - L2 (cos.phi) (PF) Instantaneous value
1.1.73.7.0.255 - L3 (cos.phi) (PF) Instantaneous value
1.1.13.7.0.255 - Avegage (cos.phi) (PF) Inst. value
1.1.22.7.0.255 - L1 Active- Instantaneous value
1.1.42.7.0.255 - L2 Active- Instantaneous value
1.1.62.7.0.255 - L3 Active- Instantaneous value
1.1.22.8.0.255 - L1 Active- Energy
1.1.42.8.0.255 - L2 Active- Energy
1.1.62.8.0.255 - L3 Active- Energy
1.1.21.8.0.255 - L1 Active+ Energy
1.1.41.8.0.255 - L2 Active+ Energy
1.1.61.8.0.255 - L3 Active+ Energy

View File

@@ -0,0 +1,112 @@
<GatewayRequest>
<NetworkId Value="231" />
<PhysicalDeviceAddress Value="" />
<DataNotification>
<LongInvokeIdAndPriority Value="40000000" />
<DateTime Value="" />
<NotificationBody>
<DataValue>
<Structure Qty="41" >
<String Value="Kamstrup_V0001" />
<!--1.1.1.8.0.255-->
<OctetString Value="0101010800FF" />
<UInt32 Value="001194CA" />
<!--1.1.2.8.0.255-->
<OctetString Value="0101020800FF" />
<UInt32 Value="00000000" />
<!--1.1.3.8.0.255-->
<OctetString Value="0101030800FF" />
<UInt32 Value="0000127E" />
<!--1.1.4.8.0.255-->
<OctetString Value="0101040800FF" />
<UInt32 Value="0009550E" />
<!--1.1.0.0.1.255-->
<OctetString Value="0101000001FF" />
<UInt32 Value="0144ADE1" />
<!--1.1.1.7.0.255-->
<OctetString Value="0101010700FF" />
<UInt32 Value="00000531" />
<!--1.1.2.7.0.255-->
<OctetString Value="0101020700FF" />
<UInt32 Value="00000000" />
<!--1.1.3.7.0.255-->
<OctetString Value="0101030700FF" />
<UInt32 Value="00000000" />
<!--1.1.4.7.0.255-->
<OctetString Value="0101040700FF" />
<UInt32 Value="00000054" />
<!--0.1.1.0.0.255-->
<OctetString Value="0001010000FF" />
<!--2020-05-12 10:24:50-->
<OctetString Value="07E4050C020A1832FF800080" />
<!--1.1.32.7.0.255-->
<OctetString Value="0101200700FF" />
<UInt16 Value="00E4" />
<!--1.1.52.7.0.255-->
<OctetString Value="0101340700FF" />
<UInt16 Value="00E5" />
<!--1.1.72.7.0.255-->
<OctetString Value="0101480700FF" />
<UInt16 Value="00E3" />
<!--1.1.31.7.0.255-->
<OctetString Value="01011F0700FF" />
<UInt32 Value="0000004B" />
<!--1.1.51.7.0.255-->
<OctetString Value="0101330700FF" />
<UInt32 Value="00000070" />
<!--1.1.71.7.0.255-->
<OctetString Value="0101470700FF" />
<UInt32 Value="000001E4" />
<!--1.1.21.7.0.255-->
<OctetString Value="0101150700FF" />
<UInt32 Value="00000070" />
<!--1.1.41.7.0.255-->
<OctetString Value="0101290700FF" />
<UInt32 Value="000000B5" />
<!--1.1.61.7.0.255-->
<OctetString Value="01013D0700FF" />
<UInt32 Value="0000040C" />
<!--1.1.33.7.0.255-->
<OctetString Value="0101210700FF" />
<UInt16 Value="004D" />
<!--1.1.53.7.0.255-->
<OctetString Value="0101350700FF" />
<UInt16 Value="004E" />
<!--1.1.73.7.0.255-->
<OctetString Value="0101490700FF" />
<UInt16 Value="0062" />
<!--1.1.13.7.0.255-->
<OctetString Value="01010D0700FF" />
<UInt16 Value="0063" />
<!--1.1.22.7.0.255-->
<OctetString Value="0101160700FF" />
<UInt32 Value="00000000" />
<!--1.1.42.7.0.255-->
<OctetString Value="01012A0700FF" />
<UInt32 Value="00000000" />
<!--1.1.62.7.0.255-->
<OctetString Value="01013E0700FF" />
<UInt32 Value="00000000" />
<!--1.1.22.8.0.255-->
<OctetString Value="0101160800FF" />
<UInt32 Value="00000000" />
<!--1.1.42.8.0.255-->
<OctetString Value="01012A0800FF" />
<UInt32 Value="00000000" />
<!--1.1.62.8.0.255-->
<OctetString Value="01013E0800FF" />
<UInt32 Value="00000000" />
<!--1.1.21.8.0.255-->
<OctetString Value="0101150800FF" />
<UInt32 Value="000A8F97" />
<!--1.1.41.8.0.255-->
<OctetString Value="0101290800FF" />
<UInt32 Value="0004C152" />
<!--1.1.61.8.0.255-->
<OctetString Value="01013D0800FF" />
<UInt32 Value="000243DF" />
</Structure>
</DataValue>
</NotificationBody>
</DataNotification>
</GatewayRequest>

Binary file not shown.

31
frames/Aidon-TN-3p.raw Normal file
View File

@@ -0,0 +1,31 @@
T FF FF DA SA SA C HC HC LD LS LQ AT AI AI AI AI AD
7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 08 64 02 02 0F 00 16 1B E1
7E A1 1E 41 08 83 13 EE EE E6 E7 00 0F 40 00 00 00 00 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B 41 49 44 4F 4E 5F 56 30 30 30 31 02 02 09 06 00 00 60 01 00 FF 0A 10 37 33 35 39 39 39 32 38 39 30 34 39 37 39 39 37 02 02 09 06 00 00 60 01 07 FF 0A 04 36 35 33 34 02 03 09 06 01 00 01 07 00 FF 06 00 00 08 6C 02 02 0F 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00 00 02 09 02 02 0F 00 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 00 41 02 02 0F FF 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 13 02 02 0F FF 16 21 02 03 09 06 01 00 47 07 00 FF 10 00 0E 02 02 0F FF 16 21 02 03 09 06 01 00 20 07 00 FF 12 08 F2 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12 08 D1 02 02 0F FF 16 23 02 03 09 06 01 00 48 07 00 FF 12 08 E8 02 02 0F FF 16 23 8B
7E A1 8A 41 08 83 13 EB FD E6 E7 00 0F 40 00 00 00 00 01 12 02 02 09 06 01 01 00 02 81 FF 0A 0B 41 49 44 4F 4E 5F 56 30 30 30 31 02 02 09 06 00 00 60 01 00 FF 0A 10 37 33 35 39 39 39 32 38 39 30 34 39 37 39 39 37 02 02 09 06 00 00 60 01 07 FF 0A 04 36 35 33 34 02 03 09 06 01 00 01 07 00 FF 06 00 00 03 9A 02 02 0F 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00 00 02 0E 02 02 0F 00 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 00 11 02 02 0F FF 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 10 02 02 0F FF 16 21 02 03 09 06 01 00 47 07 00 FF 10 00 0E 02 02 0F FF 16 21 02 03 09 06 01 00 20 07 00 FF 12 08 F4 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12 08 CD 02 02 0F FF 16 23 02 03 09 06 01 00 48 07 00 FF 12 08 DC 02 02 0F FF 16 23 02 02 09 06 00 00 01 00 00 FF 09 0C 07 E5 03 18 03 08 00 00 FF 00 00 00 02 03 09 06 01 00 01 08 00 FF 06 00 47 F0 34 02 02 0F 01 16 1E 02 03 09 06 01 00 02 08 00 FF 06 00 00 00 00 02 02 0F 01 16 1E 02 03 09 06 01 00 03 08 00 FF 06 00 00 21 9E 02 02 0F 01 16 20 02 03 09 06 01 00 04 08 00 FF 06 00 08 E0 21 02 02 0F 01 16 20 57
7E A1 8A 41 08 83 13 EB FD E6 E7 00 0F 40 00 00 00 00
01 12
02 02 09 06 01 01 00 02 81 FF 0A 0B 41 49 44 4F 4E 5F 56 30 30 30 31
02 02 09 06 00 00 60 01 00 FF 0A 10 37 33 35 39 39 39 32 38 39 30 34 39 37 39 39 37
02 02 09 06 00 00 60 01 07 FF 0A 04 36 35 33 34
02 03 09 06 01 00 01 07 00 FF 06 00 00 09 6D 02 02 0F 00 16 1B
02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B
02 03 09 06 01 00 03 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D
02 03 09 06 01 00 04 07 00 FF 06 00 00 02 5B 02 02 0F 00 16 1D
Object with three values Value Object with two values
| Obis code | | Scaling
| | | | | Unit
02 03 09 06 01 00 1F 07 00 FF 10 00 11 02 02 0F FF 16 21
02 03 09 06 01 00 33 07 00 FF 10 00 03 02 02 0F FF 16 21
02 03 09 06 01 00 47 07 00 FF 10 00 5A 02 02 0F FF 16 21
02 03 09 06 01 00 20 07 00 FF 12 09 04 02 02 0F FF 16 23
02 03 09 06 01 00 34 07 00 FF 12 09 02 02 02 0F FF 16 23
02 03 09 06 01 00 48 07 00 FF 12 08 EC 02 02 0F FF 16 23
02 02 09 06 00 00 01 00 00 FF 09 0C 07 E5 0A 1F 00 14 00 00 FF 00 00 00
02 03 09 06 01 00 01 08 00 FF 06 00 56 9F 52 02 02 0F 01 16 1E
02 03 09 06 01 00 02 08 00 FF 06 00 00 00 00 02 02 0F 01 16 1E
02 03 09 06 01 00 03 08 00 FF 06 00 00 22 D0 02 02 0F 01 16 20
02 03 09 06 01 00 04 08 00 FF 06 00 0A F5 EC 02 02 0F 01 16 20
51 D7
7E

34
frames/Kaifa-TN-3p.raw Normal file
View File

@@ -0,0 +1,34 @@
T FF FF DA SA SA C HC HC LD LS LQ AT AI AI AI AI AD
7E A0 27 01 02 01 10 5A 87 E6 E7 00 0F 40 00 00 00 09 0C 07 E5 03 17 02 13 1A 3A FF 80 00 00
02 01 // Frame type and size
06 00 00 0B F3 // Active power
5B 05 7E // CRC and end tag
T FF FF DA SA SA C HC HC LD LS LQ AT AI AI AI AI AD
7E A0 78 01 02 01 10 C4 98 E6 E7 00 0F 40 00 00 00 09 0C 07 E5 03 17 02 13 1B 00 FF 80 00 00
02 0D // Frame type and size
09 07 4B 46 4D 5F 30 30 31 // List version
09 10 XX XX XX XX XX XX XX XX XX XX 35 33 34 34 39 33 // Meter ID
09 07 4D 41 33 30 34 48 34 // Meter type
06 00 00 0C 21 // Active import
06 00 00 00 00 // Active export
06 00 00 00 00 // Reactive import
06 00 00 01 9F // Reactive export
06 00 00 0B F3 // I1
06 00 00 05 0B // I2
06 00 00 25 11 // I3
06 00 00 09 44 // U1
06 00 00 09 49 // U2
06 00 00 09 39 // U3
C9 95 7E // CRC and end tag
7E A0 9A 01 02 01 10 AA A5 E6 E7 00 0F 40 00 00 00 09 0C 07 E5 03 17 02 13 00 0A FF 80 00 00
02 12 09 07 4B 46 4D 5F 30 30 31
09 10 XX XX XX XX XX XX XX XX XX XX 35 33 34 34 39 33
09 07 4D 41 33 30 34 48 34
06 00 00 09 99
06 00 00 00 00 06 00 00 00 00 06 00 00 01 67 06 00 00 03 BF 06 00 00 05 05
06 00 00 24 34 06 00 00 09 45 06 00 00 09 4F 06 00 00 09 3B
09 0C 07 E5 03 17 02 13 00 0A FF 80 00 00 06 01 34 3B 5D 06 00 00 00 00 06 00 00 09 36 06 00 3C 7A 98 DA 15 7E
7E A0 79 01 02 01 10 80 93 E6 E7 00 0F 40 00 00 00 09 0C 07 E1 09 0E 04 15 1F 14 FF 80 00 00 02 0D 09 07 4B 46 4D 5F 30 30 31 09 10 36 39 37 30 36 33 31 34 30 31 37 35 33 39 38 35 09 08 4D 41 33 30 34 48 33 45 06 00 00 04 0C 06 00 00 00 00 06 00 00 00 00 06 00 00 00 4E 06 00 00 07 C1 06 00 00 0C 9E 06 00 00 0D 7E 06 00 00 09 5F 06 00 00 00 00 06 00 00 09 66 87 96 7E

View File

@@ -0,0 +1,51 @@
# After decode:
7E
A1 E9 // Frame type and size
41 03 13 C6 37 E6 E7 00
DB // Encrypted
08 4B 41 4D 45 01 AC 4D 6E // System title
82 // Prefix for 2-byte length
01 D0 // Length
30 // Security tag 0011 0000, 0=Compression off, 0=Unicast, 1=Encryption, 0=Authentication, 0000= Security Suite ID
00 00 A3 2F // Frame counter
// Decrypted frame below
0F 00 00 00 00
0C 07 E4 05 0C 02 0A 19 00 FF 80 00 80 // Package timestamp
02 41
0A 0E 4B 61 6D 73 74 72 75 70 5F 56 30 30 30 31 - List ID
09 06 01 01 01 08 00 FF 06 00 11 94 CA - Active+ Energy
09 06 01 01 02 08 00 FF 06 00 00 00 00 - Active- Energy
09 06 01 01 03 08 00 FF 06 00 00 12 7E - Reactive+ Energy
09 06 01 01 04 08 00 FF 06 00 09 55 0E - Reactive- Energy
09 06 01 01 00 00 01 FF 06 01 44 AD E1 - Electricity ID?
09 06 01 01 01 07 00 FF 06 00 00 05 CC - Active+ Instantaneous value
09 06 01 01 02 07 00 FF 06 00 00 00 00 - Active- Instantaneous value
09 06 01 01 03 07 00 FF 06 00 00 00 00 - Reactive+ Instantaneous value
09 06 01 01 04 07 00 FF 06 00 00 00 17 - Reactive- Instantaneous value
09 06 00 01 01 00 00 FF 09 0C 07 E4 05 0C 02 0A 19 00 FF 80 00 80 - Current date/time
09 06 01 01 20 07 00 FF 12 00 E5 - L1 Voltage Instantaneous value
09 06 01 01 34 07 00 FF 12 00 E5 - L2 Voltage Instantaneous value
09 06 01 01 48 07 00 FF 12 00 E3 - L3 Voltage Instantaneous value
09 06 01 01 1F 07 00 FF 06 00 00 00 4B - L1 Current Instantaneous value
09 06 01 01 33 07 00 FF 06 00 00 00 AA - L2 Current Instantaneous value
09 06 01 01 47 07 00 FF 06 00 00 01 E4 - L3 Current Instantaneous value
09 06 01 01 15 07 00 FF 06 00 00 00 71 - L1 Active+ Instantaneous value
09 06 01 01 29 07 00 FF 06 00 00 01 54 - L2 Active+ Instantaneous value
09 06 01 01 3D 07 00 FF 06 00 00 04 07 - L3 Active+ Instantaneous value
09 06 01 01 21 07 00 FF 12 00 4D - L1 (cos.phi) (PF) Instantaneous value
09 06 01 01 35 07 00 FF 12 00 5F - L2 (cos.phi) (PF) Instantaneous value
09 06 01 01 49 07 00 FF 12 00 62 - L3 (cos.phi) (PF) Instantaneous value
09 06 01 01 0D 07 00 FF 12 00 63 - Avegage (cos.phi) (PF) Inst. value
09 06 01 01 16 07 00 FF 06 00 00 00 00 - L1 Active- Instantaneous value
09 06 01 01 2A 07 00 FF 06 00 00 00 00 - L2 Active- Instantaneous value
09 06 01 01 3E 07 00 FF 06 00 00 00 00 - L3 Active- Instantaneous value
09 06 01 01 16 08 00 FF 06 00 00 00 00 - L1 Active- Energy
09 06 01 01 2A 08 00 FF 06 00 00 00 00 - L2 Active- Energy
09 06 01 01 3E 08 00 FF 06 00 00 00 00 - L3 Active- Energy
09 06 01 01 15 08 00 FF 06 00 0A 8F 97 - L1 Active+ Energy
09 06 01 01 29 08 00 FF 06 00 04 C1 53 - L2 Active+ Energy
09 06 01 01 3D 08 00 FF 06 00 02 43 E0 - L3 Active+ Energy
5B C3 CD 5E 79 18 18 DA 9F 97 85 FF 5A 84 7E

18
frames/Kamstup-TN-3p.raw Normal file
View File

@@ -0,0 +1,18 @@
7E A0 E2 2B 21 13 23 9A E6 E7 00 0F 00 00 00 00
0C 07 E5 0B 11 03 0B 32 00 FF 80 00 00
02 19
0A 0E 4B 61 6D 73 74 72 75 70 5F 56 30 30 30 31 - List ID
09 06 01 01 00 00 05 FF 0A 10 XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX - Meter ID
09 06 01 01 60 01 01 FF 0A 12 36 38 34 31 31 33 31 42 4E 32 34 33 31 30 31 30 34 30 - Meter model
09 06 01 01 01 07 00 FF 06 00 00 05 E6 - Active+
09 06 01 01 02 07 00 FF 06 00 00 00 00 - Active-
09 06 01 01 03 07 00 FF 06 00 00 00 00 - Reactive+
09 06 01 01 04 07 00 FF 06 00 00 01 92 - Reactive-
09 06 01 01 1F 07 00 FF 06 00 00 00 A1 - L1 current
09 06 01 01 33 07 00 FF 06 00 00 00 C1 - L2 current
09 06 01 01 47 07 00 FF 06 00 00 01 8E - L3 current
09 06 01 01 20 07 00 FF 12 00 EB - L1 voltage
09 06 01 01 34 07 00 FF 12 00 EC - L2 voltage
09 06 01 01 48 07 00 FF 12 00 EC - L3 voltage
EF 5F 7E

104
frames/austria.raw Normal file
View File

@@ -0,0 +1,104 @@
// HDLC header
68
01 01 // Format (0x00) and total length (257)
68 // Start
53 // Control field
FF // Address (Broadcast address)
// LLC
00 // Control information field
01 // Source SAP
67 // Destination SAP
DB // Encrypted
08 53 41 47 59 05 E6 D9 FD // System title
81 // Prefix for 1-byte length
F8 // Length (248), starting from 0xDB and including end byte
20 // Security tag 0010 0000, 0=Compression off, 0=Unicast, 1=Encryption, 0=No auth, 0000= Security Suite ID
00 72 00 76 // Frame counter
Some complete frames
68 01 01 68 53 FF 00 01 67 DB 08 53 41 47 59 05 E6 D9 FD 81 F8 20 00 69 D1 4F D7 32 A2 4E 08 32 D8 38 62 C0
91 7E 0F C3 BF 47 83 9A 1C 8F 81 D8 BC DB 8D C8 06 D6 8C B3 F2 7A 64 FF F5 AE F8 74 31 7F F0 D8 D8 30 57 57
D7 23 C1 5A 50 23 A2 56 C5 4E 1B A3 C1 FC 75 65 75 31 4F EF D3 71 C3 E9 B4 1E CD 61 3E BF A7 27 26 A7 48 B4
64 E3 75 B5 4A A3 57 B1 C1 8C E2 25 8F D9 14 C6 6F 9B 6B EE EF 7E 0B 3E 1C 7E 53 7F D4 A6 9D 5F 3E 5E 0B 4A
61 BA 45 8F A4 0E D5 2D 88 F3 51 76 1D 90 78 8E 0F 29 43 D4 DF 9E 05 88 26 1F C9 4A 1D F2 C2 95 84 57 A8 95
19 EF 45 7B E8 17 CE 59 B1 78 1D 0D 82 E4 58 3F 1A 76 D2 01 CF 65 75 3C 53 97 78 C0 8A 8A 31 94 E5 15 01 81
EB 58 E6 95 34 3D C9 46 AF FC 57 EE 5A 6D 5E 6F 6A 21 15 D1 6B 7D 4F E2 A1 83 C4 3A 81 CA 1E C9 D0 73 84 E1
60 E5 0E 80 BC D5 58 2D B9 1A 16
// 19b
68 0D 0D 68 53 FF 11 01 67 CD 6B CB 69 13 53 FF 98 34 16
// 263b
68 01 01 68 53 FF 00 01 67 DB 08 53 41 47 59 05
E6 D9 FD 81 F8 20 00 69 D1 50 55 28 2C E9 97 46
82 61 19 3E 23 78 8A E6 E2 42 D1 D6 44 BA 2C 3C
55 0E 59 47 02 DC 8D D4 10 91 67 6B 76 9B F0 2F
42 BF D9 D2 FE A2 B3 AA 11 B1 BF 7B 8B B3 36 FE
7E B0 22 D7 60 10 48 1B 77 AA C2 DC 99 8D C2 C4
5D 78 83 53 92 E8 66 44 CC 32 43 A9 E8 22 B2 0E
DF D8 39 B3 21 5B E6 A8 F1 83 5E 85 5A A3 5D 2B
92 ED 59 D7 24 2C CC 26 AB A6 0A FE 78 B0 E9 D3
7C 6D B8 32 0F 36 C0 A0 9B A2 56 73 08 56 EE 9B
AD 7C CC F3 6B EC 13 63 55 2A 28 0E 7A 9B D9 2A
62 08 D5 9C AD E8 43 6D 7A CA 8B DD BF DB 3F E1
88 3F 9D B9 7C 19 D3 68 8C 57 AB 82 46 4B 75 B8
F3 9E 2C 22 06 2A 93 78 56 56 76 51 65 11 C7 12
78 AB F2 97 97 51 2A 16 70 56 30 C9 12 00 08 BC
80 55 6E 44 51 A1 93 CD CF BA 9A DA CA 48 19 74
E4 70 1E AD 63 99 16
68 0D 0D 68 53 FF 11 01 67 21 2B 32 52 74 00 40 41 90 16
68 01 01 68 53 FF 00 01 67 DB 08 53
41 47 59 05 E6 D9 FD 81 F8 20 00 69 D1 51 A8 0C 89 6B 68 23 FE 94 57 3B AB 39 56 9F 26 E5 D9 A7 10 1C F3 E4
0E 2E C6 8D 3F 0A FE 8B 54 ED AC AE 84 36 86 72 B6 AA 0D B3 94 88 C0 37 4C 75 09 53 6E 3F 44 E1 A9 28 F8 28
7A D4 E0 65 0A FA 46 A9 08 A6 3A EE C6 20 B5 7C E8 F8 C1 92 40 84 54 2E F4 99 A9 04 86 42 9E 2F E3 D5 28 92
99 80 EE 10 A4 96 7F BC 72 63 33 32 4E 1C FF 71 1C 4B 66 CE 48 9B 46 FF A5 36 F2 E6 FE 84 E8 38 56 65 2E 59
79 4B 2A A1 84 B4 63 53 25 EA 02 F1 9B 50 A2 CA FB DE 22 BB E8 24 A5 70 52 F4 64 F5 93 D7 16 9D A1 90 6C F3
04 C6 26 95 6E 60 3E C6 4A F6 BA BB AC 01 FE 23 74 26 3F 7E F1 05 BD 76 2A 7C 34 FA FC EF 1F 40 46 6E F1 6F
DA 36 75 00 E6 1A BF C2 B5 7F 34 D7 37 4C A0 6C CC A7 33 EF 9C 4A B0 E7 50 67 0A FB 85 18 25 31 67 DD 16
68 0D 0D 68 53 FF 11 01 67 CD 91 DA 34 7A CE 84 AD B0 16
68 01 01 68 53 FF 00 01 67 DB 08 53 41 47 59 05 E6 D9
FD 81 F8 20 00 69 D1 52 7F 86 D1 DD 40 FD F7 2C 67 32 4F 5D 69 56 B0 8F FB 04 A9 E6 03 C6 A7 6B 7F 86 E7 BF
2D E6 44 09 42 BF C8 B3 92 D1 EB CA EF B2 78 56 14 F9 32 6B 8F A4 8E 44 DB 86 B8 D1 83 82 67 BC DF 3D 85 DE
42 9F FA 88 69 F4 06 C8 CB 4C A8 FF E6 7A 44 D3 29 97 90 CA 1B 22 F7 D8 8D 6E F7 69 34 1C 7F 6A B6 AA 95 96
D0 48 95 02 0E 76 C0 BE 31 94 A1 72 66 30 E2 72 D9 82 30 1E 9A 81 DC 23 D7 AA 10 E5 91 19 AA 5B 97 F4 51 21
17 97 E6 2E 25 D2 7C 8D E0 D8 10 19 ED B6 2A B7 29 EA 4B B1 35 F9 89 65 1A 4D 45 BE C5 3F E9 86 24 14 A3 5B
20 AD 9F 20 A5 57 9F DE 82 AD 58 4D 65 35 4D 4E 74 D2 0F EA DE 26 4F 48 AB 3D E0 91 0D 9C 1D E6 C8 2B 4F C2
3B 53 21 2A 26 82 B2 7D 9F 93 83 91 9F 4C 0B 47 3A 48 60 7B A3 12 6D 0B 9A 40 77 88 16
68 0D 0D 68 53 FF 11 01 67 C7 91 CB B9 7B 23 AC 8D 7E 16
68 01 01 68 53 FF 00 01 67 DB 08 53 41 47 59 05 E6 D9 FD 81 F8 20 00 69
D1 53 4F 0C 66 DD DE 97 31 C0 8B E5 2B CE C3 99 51 17 DE FE AB 9E 2F 1B 76 06 71 8D 2E 9D BF 54 6E E5 B3 7B
CE 05 34 58 22 6D 53 8F 95 6A 0A 6F EA 88 87 18 CD 29 B4 B1 99 8E 03 05 EB 1B 61 9B 36 09 99 E9 42 D4 D5 BC
E7 57 11 1E 95 FE 9B 76 CF C2 1A 52 EE 70 2B 1F 6A 52 CA 90 85 FF AB D1 F0 E5 20 90 82 3E 5E 2E 25 60 D0 FB
74 A1 7C A2 01 C2 AC 40 4A C6 0F 82 8C 0C CE 18 B8 18 1D EF 94 CC 54 16 D8 64 1F 60 B3 34 B8 0E 0C 10 56 11
89 D6 1E 26 91 1F 85 C4 BE 55 69 96 DA D0 D6 9E 69 4A 8F 10 BF 37 97 68 55 3E 92 B1 F1 76 21 BF 34 03 54 DB
F1 2C 23 9F B7 79 02 E8 37 DD AF 15 79 70 C4 95 C9 28 90 4E 6F BC FA 52 E7 FE 27 B5 F3 6E C4 C3 C1 37 CF C9
7C E8 B1 10 7F 6B 4D 49 88 CD 2B 60 22 51 D7 5C 5F E6 AA 0B E1 2B 16
68 0D 0D 68 53 FF 11 01 67 C5 B1 63 A0 24 39 F5 57 ED 16
68 01 01 68 53 FF 00 01 67 DB 08 53 41 47 59 05 E6 D9 FD 81 F8 20 00 69 D1 54 5B D2 4F 76
37 23 43 D5 D1 C1 02 A8 E8 91 41 6D 6C F7 E5 10 1E D5 6A 1B 73 8C 54 B1 87 32 FB 85 A6 F1 80 CE 40 B9 48 5F
54 7F 1F 6A 18 F2 54 9E ED D9 18 33 DC 52 E2 14 E1 BF 65 FE 7B 58 9B 37 89 93 8D 98 AF 63 FB 90 FA A1 02 8B
80 32 BF 7D 9D 61 42 56 F0 2E 7A D1 1B B0 88 09 DC 21 F6 6E 0E 5D D3 D9 B8 66 69 96 2D 60 CC 55 2C F1 E3 A1
5B 30 56 AB 57 FE 5D 6A 4F E4 40 CF A1 64 22 C7 4E 60 DD DA 8E 08 17 2F 49 F2 C8 0D F5 A0 ED 06 27 E0 A5 F9
1C B2 7A 97 5E 59 0A CC C8 A9 25 AB 1F 6A B4 AD DE 8B FF AF 9A 4F 7F 44 D3 00 18 34 57 E3 15 A2 DB 4E D0 2A
20 54 61 60 47 50 B9 6C 8E D0 D4 7C 27 36 0C 89 0B 82 DD 14 2D BE A1 9C D9 DF 31 F9 BF 46 A4 14 26 4D 86 04
28 54 44 F3 49 9C 12 CF 02 15 F9 4D B7 AB 4A B2 16 68 0D 0D 68 53 FF 11 01 67 70 74 A7 30 84 47 41 BE 50 16
68 01 01 68 53 FF 00 01 67 DB 08 53 41 47 59 05 E6 D9 FD 81 F8 20 00 69 D1 55 59 04 1D D6 A4 96 28 33 5D A5
A5 8F 22 2F 26 3C 77 F2 94 3C D7 ED 65 24 AA 5D 96 5E 95 71 E3 1C 99 83 40 31 A1 3F 2E BD 1F 32 A7 CE 5F 32
59 05 55 AC C9 C0 9D 2A 59 AF 09 3A 81 61 BC DB 4D 60 CF 8D 65 BF 6E 06 E5 73 59 FE 1D 1C D3 1C 08 EC 5C 8E
57 AA 3E E6 06 6D 71 45 CE AB DC 5C 25 AC 15 09 BC A2 BD E8 12 C2 92 C9 9E D1 38 A4 02 59 38 98 38 63 3B 45
B4 1A 20 4D 34 05 74 46 50 BE A5 87 D1 7A 5F 98 91 9A DA E9 FA 1E AA 72 10 58 3C 0A 5D 46 81 4E 57 B2 98 DF

32
frames/decoding.txt Normal file
View File

@@ -0,0 +1,32 @@
T = Tag
FF = Frame format (4 bit type, 1 bit segmentation, 11 bit frame length)
DA = Destination Address (1-4 bytes, LSB=1 terminates)
SA = Source address (1-4 bytes, LSB=1 terminates)
C = Control (1 byte)
HC = HCS (2 bytes)
LD = LLC Destination
LS = LLC Remote
LQ = LLC Quality
AT = Tag (0x0F = unencrypted, 0xDB = encrypted) Not really documented that well...
AI = Invoke ID and priority (4 bytes)
AD = Date and time
AS = System title
CT = Cipher frame tag ? Undocumented
CL = Lenght of Cipher frame. Length of payload will be this number - 5 (control and counter in start) - 12 (GCM tag appended after payload)
CC = Security control
CO = Invocation counter
Security control bits
01234567
00110000 (0x30)
0 = read acces
1 = write access
2 = Authenticated req
3 = Encrypted req
4 = Digitally signed req
5 = Authenticated res
6 = Encrypted res
7 = Digitally signed res

View File

@@ -1,39 +1 @@
# Hardware options
There are currently two possible hardware options for this project, both in need of external power supply. A self powered board is under development by a member of the community and is currently being tested.
## Hardware v1 by [@roarfred](https://github.com/roarfred)
Composed from a ESP12E (or F) chip, this ESP8266 based board is designed specifically for this project with an on board M-bus chip.
Building this project will require some skills in ordering and assembling electronic circuits as well as programming. No detailed instructions are available.
![The HAN Reader Hardware](v1/img/HanReaderInEnclosure.PNG)
*The completed board mounted in a [3D printed enclosure](/Enclosure)*
## Assembly of readily available modules
You can also use a ESP based development board and combine this with a M-Bus module. Here are a few boards that have been tested, each one has a dedicated firmware file in the releases section.
### ESP8266 based boards
[Wemos D1 mini](https://docs.wemos.cc/en/latest/d1/d1_mini.html)
- M-Bus connected to GPIO5 (D1)
- Jump GPIO4 (D2) to GND to force AP mode during boot
- Dallas temp sensor connected to GPIO14 (D5)
### ESP32 based boards
[Wemos D32](https://docs.wemos.cc/en/latest/d32/d32.html)
- M-Bus connected to GPIO16
- Jump GPIO4 to GND to force AP mode during boot
- Dallas temp sensor connected to GPIO14
[Adafruit HUZZAH32](https://www.adafruit.com/product/3405)
- M-Bus connected to GPIO16
Combine one of above board with an M-Bus module. Connect 3.3v and GND together between the boards and connect the TX pin from the M-Bus board to the dedicated M-Bus pin on the ESP board.
[TSS721 M-BUS module board](https://www.aliexpress.com/item/TSS721/32751482255.html)
![FeatherMbus](img/feather_3010-00_mbus_slave.jpg)
[See Hardware page in Wiki](https://github.com/gskjold/AmsToMqttBridge/wiki)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -1,197 +0,0 @@
/*
* Simple sketch to simulate reading data from a Kamstrup
* AMS Meter.
*
* Created 24. October 2017 by Roar Fredriksen
* Modified 06. November 2017 by Ruben Andreassen
*/
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <HanReader.h>
#include <Kamstrup.h>
// The HAN Port reader
HanReader hanReader;
// WiFi and MQTT endpoints
const char* ssid = "ssid";
const char* password = "password";
const char* mqtt_server = "ip or dns";
const char* mqtt_topic = "sensors/out/espams";
const char* device_name = "espams";
bool enableDebug = false;
WiFiClient espClient;
PubSubClient client(espClient);
void setup() {
//setupDebugPort(); //Comment out this line if you dont need debugging on Serial1
setupWiFi();
setupMqtt();
// initialize the HanReader
// (passing no han port, as we are feeding data manually, but provide Serial for debugging)
if (enableDebug) {
hanReader.setup(&Serial, 2400, SERIAL_8N1, &Serial1);
} else {
hanReader.setup(&Serial, 2400, SERIAL_8N1, NULL);
}
}
void setupMqtt()
{
client.setServer(mqtt_server, 1883);
}
void setupDebugPort()
{
enableDebug = true;
// Initialize the Serial port for debugging
Serial1.begin(115200);
while (!Serial1) {}
Serial1.setDebugOutput(true);
Serial1.println("Serial1");
Serial1.println("Serial debugging port initialized");
}
void setupWiFi()
{
// Initialize wifi
if (enableDebug) {
Serial1.print("Connecting to ");
Serial1.println(ssid);
}
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
if (enableDebug) Serial1.print(".");
}
if (enableDebug) {
Serial1.println("");
Serial1.println("WiFi connected");
Serial1.println("IP address: ");
Serial1.println(WiFi.localIP());
}
}
void loop() {
loopMqtt();
// Read one byte from the port, and see if we got a full package
if (hanReader.read())
{
// Get the list identifier
int listSize = hanReader.getListSize();
if (enableDebug) {
Serial1.println("");
Serial1.print("List size: ");
Serial1.print(listSize);
Serial1.print(": ");
}
// Only care for the ACtive Power Imported, which is found in the first list
if (listSize == (int)Kamstrup::List1 || listSize == (int)Kamstrup::List2)
{
// Define a json object to keep the data
StaticJsonBuffer<MQTT_MAX_PACKET_SIZE> jsonBuffer;
JsonObject& root = jsonBuffer.createObject();
// Any generic useful info here
root["dn"] = device_name;
root["up"] = millis();
// Add a sub-structure to the json object,
// to keep the data from the meter itself
JsonObject& data = root.createNestedObject("data");
data["ls"] = listSize;
data["lvi"] = hanReader.getString((int)Kamstrup_List1::ListVersionIdentifier);
data["mid"] = hanReader.getString((int)Kamstrup_List1::MeterID);
data["mt"] = hanReader.getString((int)Kamstrup_List1::MeterType);
data["t"] = hanReader.getPackageTime();
data["aip"] = hanReader.getInt((int)Kamstrup_List1::ActiveImportPower); //power
data["aep"] = hanReader.getInt((int)Kamstrup_List1::ActiveExportPower);
data["rip"] = hanReader.getInt((int)Kamstrup_List1::ReactiveImportPower);
data["rep"] = hanReader.getInt((int)Kamstrup_List1::ReactiveExportPower);
data["al1"] = (float)hanReader.getInt((int)Kamstrup_List1::CurrentL1) / 100.0;
data["al2"] = (float)hanReader.getInt((int)Kamstrup_List1::CurrentL2) / 100.0;
data["al3"] = (float)hanReader.getInt((int)Kamstrup_List1::CurrentL3) / 100.0;
data["vl1"] = hanReader.getInt((int)Kamstrup_List1::VoltageL1);
data["vl2"] = hanReader.getInt((int)Kamstrup_List1::VoltageL2);
data["vl3"] = hanReader.getInt((int)Kamstrup_List1::VoltageL3);
if (listSize == (int)Kamstrup::List2)
{
data["cl"] = hanReader.getTime((int)Kamstrup_List2::MeterClock);
data["caie"] = hanReader.getInt((int)Kamstrup_List2::CumulativeActiveImportEnergy);
data["caee"] = hanReader.getInt((int)Kamstrup_List2::CumulativeActiveExportEnergy);
data["crie"] = hanReader.getInt((int)Kamstrup_List2::CumulativeReactiveImportEnergy);
data["cree"] = hanReader.getInt((int)Kamstrup_List2::CumulativeReactiveExportEnergy);
}
if (enableDebug) {
root.printTo(Serial1);
Serial1.println("JSON length");
Serial1.println(root.measureLength());
Serial1.println("");
}
// Publish the json to the MQTT server
char msg[MQTT_MAX_PACKET_SIZE];
root.printTo(msg, MQTT_MAX_PACKET_SIZE);
bool result = client.publish(mqtt_topic, msg);
if (enableDebug) {
Serial1.println("MQTT publish result:");
Serial1.println(result);
}
}
}
}
// Ensure the MQTT lirary gets some attention too
void loopMqtt()
{
if (!client.connected()) {
reconnectMqtt();
}
client.loop();
}
void reconnectMqtt() {
// Loop until we're reconnected
while (!client.connected()) {
if (enableDebug) Serial1.print("Attempting MQTT connection...");
// Attempt to connect
if (client.connect("ESP8266Client")) {
if (enableDebug) Serial1.println("connected");
// Once connected, publish an announcement...
// client.publish("sensors", "hello world");
// ... and resubscribe
// client.subscribe("inTopic");
}
else {
if (enableDebug) {
Serial1.print("failed, rc=");
Serial1.print(client.state());
Serial1.println(" try again in 5 seconds");
}
// Wait 5 seconds before retrying
delay(5000);
}
}
}

View File

@@ -1,95 +0,0 @@
# Setup
1. Copy AmsToMqttBridge\Code\Arduino\HanReader\src to Arduino\libraries
2. Download the following libraries and put them in Arduino\libraries
- ESP8266WiFi
- PubSubClient
- ArduinoJson
3. **Set MQTT_MAX_PACKET_SIZE in PubSubClient.h to at least 512 (i used 1024)**
4. Edit the following variables in the project:
- ssid
- password
- mqtt_server
- mqtt_topic
- device_name
## Output example:
### List 1
```
{
"dn": "espams",
"up": 1475902,
"data": {
"ls": 25,
"lvi": "Kamstrup_V0001",
"mid": "5706567274389702",
"mt": "6841121BN243101040",
"t": 1510088840,
"aip": 3499,
"aep": 0,
"rip": 0,
"rep": 424,
"al1": 10.27,
"al2": 6.37,
"al3": 11.79,
"vl1": 231,
"vl2": 226,
"vl3": 231
}
}
```
### List 2
```
{
"dn": "espams",
"up": 1041212,
"data": {
"ls": 35,
"lvi": "Kamstrup_V0001",
"mid": "5706567274389702",
"mt": "6841121BN243101040",
"t": 1510088405,
"aip": 4459,
"aep": 0,
"rip": 0,
"rep": 207,
"al1": 14.72,
"al2": 6.39,
"al3": 15.02,
"vl1": 231,
"vl2": 227,
"vl3": 231,
"cl": 1510088405,
"caie": 588500,
"caee": 0,
"crie": 93,
"cree": 80831
}
}
```
### List 1 and 2 fields
- dn = Device Name
- up = MS since last reboot
- ls = List Size
- lvi = List Version Identifier
- mid = Meter ID
- mt = Meter Type
- t = Time
- aie = Active Import Power
- aep = Active Export Power
- rip = Reactive Import Power
- rep = Reactive Export Power
- al1 = Current L1
- al2 = Current L2
- al3 = Current L3
- cl1 = Voltage L1
- cl2 = Voltage L2
- cl3 = Voltage L3
### List 2 additional fields
- cl = Meter Clock
- caie = Cumulative Active Import Energy
- caee = Cumulative Active Export Energy
- crie = Cumulative Reactive Import Energy
- cree = Cumulative Reactive Export Energy

View File

@@ -1,61 +0,0 @@
/*
* Simple sketch to read MBus data from electrical meter
* As the protocol requires "Even" parity, and this is
* only supported on the hardware port of the ESP8266,
* we'll have to use Serial1 for debugging.
*
* This means you'll have to program the ESP using the
* regular RX/TX port, and then you must remove the FTDI
* and connect the MBus signal from the meter to the
* RS pin. The FTDI/RX can be moved to Pin2 for debugging
*
* Created 14. september 2017 by Roar Fredriksen
*/
#include "HanReader.h"
#include "Kaifa.h"
// The HAN Port reader
HanReader hanReader;
void setup() {
setupDebugPort();
// initialize the HanReader
// (passing Serial as the HAN port and Serial1 for debugging)
hanReader.setup(&Serial, &Serial1);
}
void setupDebugPort()
{
// Initialize the Serial1 port for debugging
// (This port is fixed to Pin2 of the ESP8266)
Serial1.begin(115200);
while (!Serial1) {}
Serial1.setDebugOutput(true);
Serial1.println("Serial1");
Serial1.println("Serial debugging port initialized");
}
void loop() {
// Read one byte from the port, and see if we got a full package
if (hanReader.read())
{
// Get the list identifier
int listSize = hanReader.getListSize();
Serial1.println("");
Serial1.print("List size: ");
Serial1.print(listSize);
Serial1.print(": ");
// Only care for the ACtive Power Imported, which is found in the first list
if (listSize == (int)Kaifa::List1)
{
int power = hanReader.getInt((int)Kaifa_List1::ActivePowerImported);
Serial1.print("Power consumtion is right now: ");
Serial1.print(power);
Serial1.println(" W");
}
}
}

View File

@@ -1,202 +0,0 @@
/*
* Simple sketch to read MBus data from electrical meter
* As the protocol requires "Even" parity, and this is
* only supported on the hardware port of the ESP8266,
* we'll have to use Serial1 for debugging.
*
* This means you'll have to program the ESP using the
* regular RX/TX port, and then you must remove the FTDI
* and connect the MBus signal from the meter to the
* RS pin. The FTDI/RX can be moved to Pin2 for debugging
*
* Created 14. september 2017 by Roar Fredriksen
*/
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include "HanReader.h"
#include "Kaifa.h"
// The HAN Port reader
HanReader hanReader;
// WiFi and MQTT endpoints
const char* ssid = "Roar_Etne";
const char* password = "**********";
const char* mqtt_server = "192.168.10.203";
WiFiClient espClient;
PubSubClient client(espClient);
void setup() {
setupDebugPort();
setupWiFi();
setupMqtt();
// initialize the HanReader
// (passing Serial as the HAN port and Serial1 for debugging)
hanReader.setup(&Serial, &Serial1);
}
void setupMqtt()
{
client.setServer(mqtt_server, 1883);
}
void setupDebugPort()
{
// Initialize the Serial1 port for debugging
// (This port is fixed to Pin2 of the ESP8266)
Serial1.begin(115200);
while (!Serial1) {}
Serial1.setDebugOutput(true);
Serial1.println("Serial1");
Serial1.println("Serial debugging port initialized");
}
void setupWiFi()
{
// Initialize wifi
Serial1.print("Connecting to ");
Serial1.println(ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial1.print(".");
}
Serial1.println("");
Serial1.println("WiFi connected");
Serial1.println("IP address: ");
Serial1.println(WiFi.localIP());
}
void loop() {
loopMqtt();
// Read one byt from the port, and see if we got a full package
if (hanReader.read())
{
// Get the list identifier
int listSize = hanReader.getListSize();
Serial1.println("");
Serial1.print("List size: ");
Serial1.print(listSize);
Serial1.print(": ");
// Only care for the ACtive Power Imported, which is found in the first list
if (listSize == (int)Kaifa::List1 || listSize == (int)Kaifa::List2 || listSize == (int)Kaifa::List3)
{
if (listSize == (int)Kaifa::List1)
{
Serial1.println(" (list #1 has no ID)");
}
else
{
String id = hanReader.getString((int)Kaifa_List2::ListVersionIdentifier);
Serial1.println(id);
}
// Get the timestamp (as unix time) from the package
time_t time = hanReader.getPackageTime();
Serial.print("Time of the package is: ");
Serial.println(time);
// Define a json object to keep the data
StaticJsonBuffer<500> jsonBuffer;
JsonObject& root = jsonBuffer.createObject();
// Any generic useful info here
root["id"] = "espdebugger";
root["up"] = millis();
root["t"] = time;
// Add a sub-structure to the json object,
// to keep the data from the meter itself
JsonObject& data = root.createNestedObject("data");
// Based on the list number, get all details
// according to OBIS specifications for the meter
if (listSize == (int)Kaifa::List1)
{
data["P"] = hanReader.getInt((int)Kaifa_List1::ActivePowerImported);
}
else if (listSize == (int)Kaifa::List2)
{
data["lv"] = hanReader.getString((int)Kaifa_List2::ListVersionIdentifier);
data["id"] = hanReader.getString((int)Kaifa_List2::MeterID);
data["type"] = hanReader.getString((int)Kaifa_List2::MeterType);
data["P"] = hanReader.getInt((int)Kaifa_List2::ActiveImportPower);
data["Q"] = hanReader.getInt((int)Kaifa_List2::ReactiveImportPower);
data["I1"] = hanReader.getInt((int)Kaifa_List2::CurrentL1);
data["I2"] = hanReader.getInt((int)Kaifa_List2::CurrentL2);
data["I3"] = hanReader.getInt((int)Kaifa_List2::CurrentL3);
data["U1"] = hanReader.getInt((int)Kaifa_List2::VoltageL1);
data["U2"] = hanReader.getInt((int)Kaifa_List2::VoltageL2);
data["U3"] = hanReader.getInt((int)Kaifa_List2::VoltageL3);
}
else if (listSize == (int)Kaifa::List3)
{
data["lv"] = hanReader.getString((int)Kaifa_List3::ListVersionIdentifier);;
data["id"] = hanReader.getString((int)Kaifa_List3::MeterID);
data["type"] = hanReader.getString((int)Kaifa_List3::MeterType);
data["P"] = hanReader.getInt((int)Kaifa_List3::ActiveImportPower);
data["Q"] = hanReader.getInt((int)Kaifa_List3::ReactiveImportPower);
data["I1"] = hanReader.getInt((int)Kaifa_List3::CurrentL1);
data["I2"] = hanReader.getInt((int)Kaifa_List3::CurrentL2);
data["I3"] = hanReader.getInt((int)Kaifa_List3::CurrentL3);
data["U1"] = hanReader.getInt((int)Kaifa_List3::VoltageL1);
data["U2"] = hanReader.getInt((int)Kaifa_List3::VoltageL2);
data["U3"] = hanReader.getInt((int)Kaifa_List3::VoltageL3);
data["tPI"] = hanReader.getInt((int)Kaifa_List3::CumulativeActiveImportEnergy);
data["tPO"] = hanReader.getInt((int)Kaifa_List3::CumulativeActiveExportEnergy);
data["tQI"] = hanReader.getInt((int)Kaifa_List3::CumulativeReactiveImportEnergy);
data["tQO"] = hanReader.getInt((int)Kaifa_List3::CumulativeReactiveExportEnergy);
}
// Write the json to the debug port
root.printTo(Serial1);
Serial1.println();
// Publish the json to the MQTT server
char msg[1024];
root.printTo(msg, 1024);
client.publish("sensors/out/espdebugger", msg);
}
}
}
// Ensure the MQTT lirary gets some attention too
void loopMqtt()
{
if (!client.connected()) {
reconnectMqtt();
}
client.loop();
}
void reconnectMqtt() {
// Loop until we're reconnected
while (!client.connected()) {
Serial1.print("Attempting MQTT connection...");
// Attempt to connect
if (client.connect("ESP8266Client")) {
Serial1.println("connected");
// Once connected, publish an announcement...
// client.publish("sensors", "hello world");
// ... and resubscribe
// client.subscribe("inTopic");
}
else {
Serial1.print("failed, rc=");
Serial1.print(client.state());
Serial1.println(" try again in 5 seconds");
// Wait 5 seconds before retrying
delay(5000);
}
}
}

View File

@@ -1,10 +0,0 @@
name=HANreader
version=1.0.1
author=roarfred
maintainer=roarfred <not@important.com>
sentence=HAN support
paragraph=HAN support
category=Sensors
url=https://github.com/roarfred/AmsToMqttBridge
architectures=*
depends=Timezone

View File

@@ -1,19 +0,0 @@
Arduino Compatible Cross Platform C++ Library Project : For more information see http://www.visualmicro.com
This project works exactly the same way as an Arduino library.
Add this project to any solution that contains an Arduino project and #include <headers.h> in code as you would any normal Arduino library headers.
To enable intellisense and to support live build discovery outside of the "standard" Arduino library locations, ensure that the library is added as a shared project reference to the master Arduino project. To do this, right click the master project "References" node and then click "Add Reference". A window will open and the library will appear on the "Shared Projects" tab. Click the checkbox next to the library name to add the reference. If this library is moved the shared referencemust be removed and re-added.
VS2017 has a bug, workround: After moving existing source code within a "library or shared project", close and re-open the solution.
Visual Studio will display intellisense for libraries based on the platform/board that has been specified for the currently active "Startup Project" of the current solution.
IMPORTANT: The arduino.cc Library Rules must be followed when adding code or restructing libraries.
blog: http://www.visualmicro.com/post/2017/01/16/Arduino-Cross-Platform-Library-Development.aspx

View File

@@ -1,304 +0,0 @@
// Aidon.h
#ifndef _AIDON_h
#define _AIDON_h
enum class Aidon
{
List1 = 0x01,
List1PhaseShort = 0x09,
List1PhaseLong = 0x0E,
List3PhaseShort = 0x0D,
List3PhaseLong = 0x12,
List3PhaseITShort = 0x0C,
List3PhaseITLong = 0x11,
};
enum class Aidon_List1
{
ListSize,
IGN_0,
ActiveImportPower_OBIS,
ActiveImportPower,
IGN_1,
ActiveImportPowerInt8,
ActiveImportPowerEnum
};
enum class Aidon_List1Phase
{
ListSize,
IGN_0,
ListVersionIdentifier_OBIS,
ListVersionIdentifier,
IGN_1,
MeterID_OBIS,
MeterID,
IGN_2,
MeterType_OBIS,
MeterType,
IGN_3,
ActiveImportPower_OBIS,
ActiveImportPower,
IGN_4,
ActiveImportPowerInt8,
ActiveImportPowerEnum,
IGN_5,
ActiveExportPower_OBIS,
ActiveExportPower,
IGN_6,
ActiveExportPowerInt8,
ActiveExportPowerEnum,
IGN_7,
ReactiveImportPower_OBIS,
ReactiveImportPower,
IGN_8,
ReactiveImportPowerInt8,
ReactiveImportPowerEnum,
IGN_9,
ReactiveExportPower_OBIS,
ReactiveExportPower,
IGN_10,
ReactiveExportPowerInt8,
ReactiveExportPowerEnum,
IGN_11,
CurrentL1_OBIS,
CurrentL1,
IGN_12,
CurrentL1Int8,
CurrentL1Enum,
IGN_13,
VoltageL1_OBIS,
VoltageL1,
IGN_14,
VoltageL1Int8,
VoltageL1Enum,
IGN_15,
Timestamp_OBIS,
Timestamp,
IGN_16,
CumulativeActiveImportEnergy_OBIS,
CumulativeActiveImportEnergy,
IGN_17,
CumulativeActiveImportEnergyInt8,
CumulativeActiveImportEnergyEnum,
IGN_18,
CumulativeActiveExportEnergy_OBIS,
CumulativeActiveExportEnergy,
IGN_19,
CumulativeActiveExportEnergyInt8,
CumulativeActiveExportEnergyEnum,
IGN_20,
CumulativeReactiveImportEnergy_OBIS,
CumulativeReactiveImportEnergy,
IGN_21,
CumulativeReactiveImportEnergyInt8,
CumulativeReactiveImportEnergyEnum,
IGN_22,
CumulativeReactiveExportEnergy_OBIS,
CumulativeReactiveExportEnergy,
IGN_23,
CumulativeReactiveExportEnergyInt8,
CumulativeReactiveExportEnergyEnum
};
enum class Aidon_List3Phase
{
ListSize,
IGN_0,
ListVersionIdentifier_OBIS,
ListVersionIdentifier,
IGN_1,
MeterID_OBIS,
MeterID,
IGN_2,
MeterType_OBIS,
MeterType,
IGN_3,
ActiveImportPower_OBIS,
ActiveImportPower,
IGN_4,
ActiveImportPowerInt8,
ActiveImportPowerEnum,
IGN_5,
ActiveExportPower_OBIS,
ActiveExportPower,
IGN_6,
ActiveExportPowerInt8,
ActiveExportPowerEnum,
IGN_7,
ReactiveImportPower_OBIS,
ReactiveImportPower,
IGN_8,
ReactiveImportPowerInt8,
ReactiveImportPowerEnum,
IGN_9,
ReactiveExportPower_OBIS,
ReactiveExportPower,
IGN_10,
ReactiveExportPowerInt8,
ReactiveExportPowerEnum,
IGN_11,
CurrentL1_OBIS,
CurrentL1,
IGN_12,
CurrentL1Int8,
CurrentL1Enum,
IGN_13,
CurrentL2_OBIS,
CurrentL2,
IGN_14,
CurrentL2Int8,
CurrentL2Enum,
IGN_15,
CurrentL3_OBIS,
CurrentL3,
IGN_16,
CurrentL3Int8,
CurrentL3Enum,
IGN_17,
VoltageL1_OBIS,
VoltageL1,
IGN_18,
VoltageL1Int8,
VoltageL1Enum,
IGN_19,
VoltageL2_OBIS,
VoltageL2,
IGN_20,
VoltageL2Int8,
VoltageL2Enum,
IGN_21,
VoltageL3_OBIS,
VoltageL3,
IGN_22,
VoltageL3Int8,
VoltageL3Enum,
IGN_23,
Timestamp_OBIS,
Timestamp,
IGN_24,
CumulativeActiveImportEnergy_OBIS,
CumulativeActiveImportEnergy,
IGN_25,
CumulativeActiveImportEnergyInt8,
CumulativeActiveImportEnergyEnum,
IGN_26,
CumulativeActiveExportEnergy_OBIS,
CumulativeActiveExportEnergy,
IGN_27,
CumulativeActiveExportEnergyInt8,
CumulativeActiveExportEnergyEnum,
IGN_28,
CumulativeReactiveImportEnergy_OBIS,
CumulativeReactiveImportEnergy,
IGN_29,
CumulativeReactiveImportEnergyInt8,
CumulativeReactiveImportEnergyEnum,
IGN_30,
CumulativeReactiveExportEnergy_OBIS,
CumulativeReactiveExportEnergy,
IGN_31,
CumulativeReactiveExportEnergyInt8,
CumulativeReactiveExportEnergyEnum
};
enum class Aidon_List3PhaseIT
{
ListSize,
IGN_0,
ListVersionIdentifier_OBIS,
ListVersionIdentifier,
IGN_1,
MeterID_OBIS,
MeterID,
IGN_2,
MeterType_OBIS,
MeterType,
IGN_3,
ActiveImportPower_OBIS,
ActiveImportPower,
IGN_4,
ActiveImportPowerInt8,
ActiveImportPowerEnum,
IGN_5,
ActiveExportPower_OBIS,
ActiveExportPower,
IGN_6,
ActiveExportPowerInt8,
ActiveExportPowerEnum,
IGN_7,
ReactiveImportPower_OBIS,
ReactiveImportPower,
IGN_8,
ReactiveImportPowerInt8,
ReactiveImportPowerEnum,
IGN_9,
ReactiveExportPower_OBIS,
ReactiveExportPower,
IGN_10,
ReactiveExportPowerInt8,
ReactiveExportPowerEnum,
IGN_11,
CurrentL1_OBIS,
CurrentL1,
IGN_12,
CurrentL1Int8,
CurrentL1Enum,
IGN_13,
CurrentL3_OBIS,
CurrentL3,
IGN_14,
CurrentL3Int8,
CurrentL3Enum,
IGN_15,
VoltageL1_OBIS,
VoltageL1,
IGN_16,
VoltageL1Int8,
VoltageL1Enum,
IGN_17,
VoltageL2_OBIS,
VoltageL2,
IGN_18,
VoltageL2Int8,
VoltageL2Enum,
IGN_19,
VoltageL3_OBIS,
VoltageL3,
IGN_20,
VoltageL3Int8,
VoltageL3Enum,
IGN_21,
Timestamp_OBIS,
Timestamp,
IGN_22,
CumulativeActiveImportEnergy_OBIS,
CumulativeActiveImportEnergy,
IGN_23,
CumulativeActiveImportEnergyInt8,
CumulativeActiveImportEnergyEnum,
IGN_24,
CumulativeActiveExportEnergy_OBIS,
CumulativeActiveExportEnergy,
IGN_25,
CumulativeActiveExportEnergyInt8,
CumulativeActiveExportEnergyEnum,
IGN_26,
CumulativeReactiveImportEnergy_OBIS,
CumulativeReactiveImportEnergy,
IGN_27,
CumulativeReactiveImportEnergyInt8,
CumulativeReactiveImportEnergyEnum,
IGN_28,
CumulativeReactiveExportEnergy_OBIS,
CumulativeReactiveExportEnergy,
IGN_29,
CumulativeReactiveExportEnergyInt8,
CumulativeReactiveExportEnergyEnum
};
#endif

View File

@@ -1,37 +0,0 @@
#include "Crc16.h"
Crc16Class::Crc16Class()
{
unsigned short value;
unsigned short temp;
for (unsigned short i = 0; i < 256; ++i)
{
value = 0;
temp = i;
for (byte j = 0; j < 8; ++j)
{
if (((value ^ temp) & 0x0001) != 0)
{
value = (ushort)((value >> 1) ^ polynomial);
}
else
{
value >>= 1;
}
temp >>= 1;
}
table[i] = value;
}
}
unsigned short Crc16Class::ComputeChecksum(byte *data, int start, int length)
{
ushort fcs = 0xffff;
for (int i = start; i < (start + length); i++)
{
byte index = (fcs ^ data[i]) & 0xff;
fcs = (ushort)((fcs >> 8) ^ table[index]);
}
fcs ^= 0xffff;
return fcs;
}

View File

@@ -1,23 +0,0 @@
#ifndef _CRC16_h
#define _CRC16_h
#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif
class Crc16Class
{
public:
Crc16Class();
unsigned short ComputeChecksum(byte *data, int start, int length);
protected:
private:
const unsigned short polynomial = 0x8408;
unsigned short table[256];
};
#endif

View File

@@ -1,154 +0,0 @@
#include "DlmsReader.h"
DlmsReader::DlmsReader()
{
//this->Clear();
}
void DlmsReader::Clear()
{
this->position = 0;
this->dataLength = 0;
this->destinationAddressLength = 0;
this->sourceAddressLength = 0;
this->frameFormatType = 0;
}
bool DlmsReader::Read(byte data)
{
if (position == 0 && data != 0x7E)
{
// we haven't started yet, wait for the start flag (no need to capture any data yet)
return false;
}
else
{
// We have completed reading of one package, so clear and be ready for the next
if (dataLength > 0 && position >= dataLength + 2)
Clear();
// Check if we're about to run into a buffer overflow
if (position >= DLMS_READER_BUFFER_SIZE)
Clear();
// Check if this is a second start flag, which indicates the previous one was a stop from the last package
if (position == 1 && data == 0x7E)
{
// just return, we can keep the one byte we had in the buffer
return false;
}
// We have started, so capture every byte
buffer[position++] = data;
if (position == 1)
{
// This was the start flag, we're not done yet
return false;
}
else if (position == 2)
{
// Capture the Frame Format Type
frameFormatType = (byte)(data & 0xF0);
if (!IsValidFrameFormat(frameFormatType))
Clear();
return false;
}
else if (position == 3)
{
// Capture the length of the data package
dataLength = ((buffer[1] & 0x0F) << 8) | buffer[2];
return false;
}
else if (destinationAddressLength == 0)
{
// Capture the destination address
destinationAddressLength = GetAddress(3, destinationAddress, 0, DLMS_READER_MAX_ADDRESS_SIZE);
if (destinationAddressLength > 3)
Clear();
return false;
}
else if (sourceAddressLength == 0)
{
// Capture the source address
sourceAddressLength = GetAddress(3 + destinationAddressLength, sourceAddress, 0, DLMS_READER_MAX_ADDRESS_SIZE);
if (sourceAddressLength > 3)
Clear();
return false;
}
else if (position == 4 + destinationAddressLength + sourceAddressLength + 2)
{
// Verify the header checksum
ushort headerChecksum = GetChecksum(position - 3);
if (headerChecksum != Crc16.ComputeChecksum(buffer, 1, position - 3))
Clear();
return false;
}
else if (position == dataLength + 1)
{
// Verify the data package checksum
ushort checksum = this->GetChecksum(position - 3);
if (checksum != Crc16.ComputeChecksum(buffer, 1, position - 3))
Clear();
return false;
}
else if (position == dataLength + 2)
{
// We're done, check the stop flag and signal we're done
if (data == 0x7E)
return true;
else
{
Clear();
return false;
}
}
}
return false;
}
bool DlmsReader::IsValidFrameFormat(byte frameFormatType)
{
return frameFormatType == 0xA0;
}
int DlmsReader::GetRawData(byte *dataBuffer, int start, int length)
{
if (dataLength > 0 && position == dataLength + 2)
{
int headerLength = 3 + destinationAddressLength + sourceAddressLength + 2;
int bytesWritten = 0;
for (int i = headerLength + 1; i < dataLength - 1; i++)
{
dataBuffer[i + start - headerLength - 1] = buffer[i];
bytesWritten++;
}
return bytesWritten;
}
else
return 0;
}
int DlmsReader::GetAddress(int addressPosition, byte* addressBuffer, int start, int length)
{
int addressBufferPos = start;
for (int i = addressPosition; i < position; i++)
{
addressBuffer[addressBufferPos++] = buffer[i];
// LSB=1 means this was the last address byte
if ((buffer[i] & 0x01) == 0x01)
break;
// See if we've reached last byte, try again when we've got more data
else if (i == position - 1)
return 0;
}
return addressBufferPos - start;
}
ushort DlmsReader::GetChecksum(int checksumPosition)
{
return (ushort)(buffer[checksumPosition + 2] << 8 |
buffer[checksumPosition + 1]);
}

View File

@@ -1,42 +0,0 @@
#ifndef _DLMSREADER_h
#define _DLMSREADER_h
#include "Crc16.h"
#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif
#define DLMS_READER_BUFFER_SIZE 512
#define DLMS_READER_MAX_ADDRESS_SIZE 5
class DlmsReader
{
public:
DlmsReader();
bool Read(byte data);
int GetRawData(byte *buffer, int start, int length);
protected:
Crc16Class Crc16;
private:
byte buffer[DLMS_READER_BUFFER_SIZE];
int position;
int dataLength;
byte frameFormatType;
byte destinationAddress[DLMS_READER_MAX_ADDRESS_SIZE];
byte destinationAddressLength;
byte sourceAddress[DLMS_READER_MAX_ADDRESS_SIZE];
byte sourceAddressLength;
void Clear();
int GetAddress(int addressPosition, byte* buffer, int start, int length);
unsigned short GetChecksum(int checksumPosition);
bool IsValidFrameFormat(byte frameFormatType);
void WriteBuffer();
};
#endif

View File

@@ -1,260 +0,0 @@
#include "HanReader.h"
HanReader::HanReader()
{
}
void HanReader::setup(Stream *hanPort, RemoteDebug *debug)
{
han = hanPort;
bytesRead = 0;
debugger = debug;
// Central European Time (Frankfurt, Paris)
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; // Central European Summer Time
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; // Central European Standard Time
localZone = new Timezone(CEST, CET);
if (debug) debug->println("MBUS serial setup complete");
}
void HanReader::setup(Stream *hanPort)
{
setup(hanPort, NULL);
}
bool HanReader::read(byte data) {
if (reader.Read(data)) {
bytesRead = reader.GetRawData(buffer, 0, 512);
if (debugger->isActive(RemoteDebug::INFO)) {
printI("Got valid DLMS data (%d bytes)", bytesRead);
if (debugger->isActive(RemoteDebug::DEBUG)) {
debugPrint(buffer, 0, bytesRead);
}
}
/*
Data should start with E6 E7 00 0F
and continue with four bytes for the InvokeId
*/
if (bytesRead < 9) {
printW("Invalid HAN data: Less than 9 bytes received");
return false;
}
else if (
buffer[0] != 0xE6 ||
buffer[1] != 0xE7 ||
buffer[2] != 0x00 ||
buffer[3] != 0x0F
)
{
printW("Invalid HAN data: Start should be E6 E7 00 0F");
return false;
}
listSize = getInt(0, buffer, 0, bytesRead);
printI("HAN data is valid, listSize: %d", listSize);
return true;
}
return false;
}
void HanReader::debugPrint(byte *buffer, int start, int length) {
for (int i = start; i < start + length; i++) {
if (buffer[i] < 0x10)
debugger->print("0");
debugger->print(buffer[i], HEX);
debugger->print(" ");
if ((i - start + 1) % 16 == 0)
debugger->println("");
else if ((i - start + 1) % 4 == 0)
debugger->print(" ");
yield(); // Let other get some resources too
}
debugger->println("");
}
bool HanReader::read() {
while(han->available()) {
if(read(han->read())) {
return true;
}
}
return false;
}
int HanReader::getListSize() {
return listSize;
}
time_t HanReader::getPackageTime() {
int packageTimePosition = dataHeader
+ (compensateFor09HeaderBug ? 1 : 0);
return getTime(buffer, packageTimePosition, bytesRead);
}
time_t HanReader::getTime(int objectId) {
return getTime(objectId, buffer, 0, bytesRead);
}
int HanReader::getInt(int objectId) {
return getInt(objectId, buffer, 0, bytesRead);
}
String HanReader::getString(int objectId) {
return getString(objectId, buffer, 0, bytesRead);
}
int HanReader::findValuePosition(int dataPosition, byte *buffer, int start, int length) {
// The first byte after the header gives the length
// of the extended header information (variable)
int headerSize = dataHeader + (compensateFor09HeaderBug ? 1 : 0);
int firstData = headerSize + buffer[headerSize] + 1;
for (int i = start + firstData; i<length; i++) {
if (dataPosition-- == 0)
return i;
else if (buffer[i] == 0x00) // null
i += 0;
else if (buffer[i] == 0x0A) // String
i += buffer[i + 1] + 1;
else if (buffer[i] == 0x09) // byte array
i += buffer[i + 1] + 1;
else if (buffer[i] == 0x01) // array (1 byte for reading size)
i += 1;
else if (buffer[i] == 0x02) // struct (1 byte for reading size)
i += 1;
else if (buffer[i] == 0x10) // int16 value (2 bytes)
i += 2;
else if (buffer[i] == 0x12) // uint16 value (2 bytes)
i += 2;
else if (buffer[i] == 0x06) // uint32 value (4 bytes)
i += 4;
else if (buffer[i] == 0x0F) // int8 value (1 bytes)
i += 1;
else if (buffer[i] == 0x16) // enum (1 bytes)
i += 1;
else {
printW("Unknown data type found: 0x%s", String(buffer[i], HEX).c_str());
return 0; // unknown data type found
}
}
printD("Passed the end of the data. Length was: %d", length);
return 0;
}
time_t HanReader::getTime(int dataPosition, byte *buffer, int start, int length) {
// TODO: check if the time is represented always as a 12 byte string (0x09 0x0C)
int timeStart = findValuePosition(dataPosition, buffer, start, length);
timeStart += 1;
return getTime(buffer, start + timeStart, length - timeStart);
}
time_t HanReader::getTime(byte *buffer, int start, int length) {
int pos = start;
int dataLength = buffer[pos++];
if (dataLength == 0x0C) {
int year = buffer[pos] << 8 |
buffer[pos + 1];
int month = buffer[pos + 2];
int day = buffer[pos + 3];
int hour = buffer[pos + 5];
int minute = buffer[pos + 6];
int second = buffer[pos + 7];
tmElements_t tm;
tm.Year = year - 1970;
tm.Month = month;
tm.Day = day;
tm.Hour = hour;
tm.Minute = minute;
tm.Second = second;
return localZone->toUTC(makeTime(tm));
} else if(dataLength == 0) {
return (time_t)0L;
} else {
printW("Unknown time length: %d", dataLength);
// Date format not supported
return (time_t)0L;
}
}
int HanReader::getInt(int dataPosition, byte *buffer, int start, int length) {
int valuePosition = findValuePosition(dataPosition, buffer, start, length);
if (valuePosition > 0) {
int value = 0;
int bytes = 0;
switch (buffer[valuePosition++]) {
case 0x10:
bytes = 2;
break;
case 0x12:
bytes = 2;
break;
case 0x06:
bytes = 4;
break;
case 0x02:
bytes = 1;
break;
case 0x01:
bytes = 1;
break;
case 0x0F:
bytes = 1;
break;
case 0x16:
bytes = 1;
break;
}
for (int i = valuePosition; i < valuePosition + bytes; i++) {
value = value << 8 | buffer[i];
}
return value;
}
return 0;
}
String HanReader::getString(int dataPosition, byte *buffer, int start, int length) {
int valuePosition = findValuePosition(dataPosition, buffer, start, length);
if (valuePosition > 0) {
String value = String("");
for (int i = valuePosition + 2; i < valuePosition + buffer[valuePosition + 1] + 2; i++) {
value += String((char)buffer[i]);
}
return value;
}
return String("");
}
void HanReader::printD(String fmt, int arg) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(String("(HanReader)" + fmt + "\n").c_str(), arg);
}
void HanReader::printI(String fmt, int arg) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf(String("(HanReader)" + fmt + "\n").c_str(), arg);
}
void HanReader::printW(String fmt, int arg) {
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf(String("(HanReader)" + fmt + "\n").c_str(), arg);
}
void HanReader::printW(String fmt, const char* arg) {
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf(String("(HanReader)" + fmt + "\n").c_str(), arg);
}
void HanReader::printE(String fmt, int arg) {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(String("(HanReader)" + fmt + "\n").c_str(), arg);
}

View File

@@ -1,60 +0,0 @@
#ifndef _HANREADER_h
#define _HANREADER_h
#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif
#include "DlmsReader.h"
#include <Timezone.h>
#include "RemoteDebug.h"
class HanReader
{
public:
const uint dataHeader = 8;
bool compensateFor09HeaderBug = false;
HanReader();
void setup(Stream *hanPort);
void setup(Stream *hanPort, RemoteDebug *debug);
bool read();
bool read(byte data);
int getListSize();
time_t getPackageTime();
int getInt(int objectId);
String getString(int objectId);
time_t getTime(int objectId);
private:
RemoteDebug* debugger;
Stream *han;
byte buffer[512];
int bytesRead;
DlmsReader reader;
int listSize;
Timezone *localZone;
int findValuePosition(int dataPosition, byte *buffer, int start, int length);
time_t getTime(int dataPosition, byte *buffer, int start, int length);
time_t getTime(byte *buffer, int start, int length);
int getInt(int dataPosition, byte *buffer, int start, int length);
String getString(int dataPosition, byte *buffer, int start, int length);
time_t toUnixTime(int year, int month, int day, int hour, int minute, int second);
void debugPrint(byte *buffer, int start, int length);
void printD(String fmt, int arg=0);
void printI(String fmt, int arg=0);
void printW(String fmt, int arg=0);
void printW(String fmt, const char* arg);
void printE(String fmt, int arg=0);
};
#endif

View File

@@ -1,57 +0,0 @@
#ifndef _KAIFA_h
#define _KAIFA_h
enum class Kaifa : byte {
List1 = 0x01,
List1PhaseShort = 0x09,
List3PhaseShort = 0x0D,
List1PhaseLong = 0x0E,
List3PhaseLong = 0x12
};
enum class Kaifa_List1 {
ListSize,
ActivePowerImported
};
enum class Kaifa_List3Phase {
ListSize,
ListVersionIdentifier,
MeterID,
MeterType,
ActiveImportPower,
ActiveExportPower,
ReactiveImportPower,
ReactiveExportPower,
CurrentL1,
CurrentL2,
CurrentL3,
VoltageL1,
VoltageL2,
VoltageL3,
MeterClock,
CumulativeActiveImportEnergy,
CumulativeActiveExportEnergy,
CumulativeReactiveImportEnergy,
CumulativeReactiveExportEnergy
};
enum class Kaifa_List1Phase {
ListSize,
ListVersionIdentifier,
MeterID,
MeterType,
ActiveImportPower,
ActiveExportPower,
ReactiveImportPower,
ReactiveExportPower,
CurrentL1,
VoltageL1,
MeterClock,
CumulativeActiveImportEnergy,
CumulativeActiveExportEnergy,
CumulativeReactiveImportEnergy,
CumulativeReactiveExportEnergy
};
#endif

View File

@@ -1,87 +0,0 @@
// Kamstrup.h
#ifndef _KAMSTRUP_h
#define _KAMSTRUP_h
enum class Kamstrup
{
List3PhaseShort = 0x19,
List3PhaseLong = 0x23,
List1PhaseShort = 0x11,
List1PhaseLong = 0x1B
};
enum class Kamstrup_List3Phase
{
ListSize,
ListVersionIdentifier,
MeterID_OBIS,
MeterID,
MeterType_OBIS,
MeterType,
ActiveImportPower_OBIS,
ActiveImportPower,
ActiveExportPower_OBIS,
ActiveExportPower,
ReactiveImportPower_OBIS,
ReactiveImportPower,
ReactiveExportPower_OBIS,
ReactiveExportPower,
CurrentL1_OBIS,
CurrentL1,
CurrentL2_OBIS,
CurrentL2,
CurrentL3_OBIS,
CurrentL3,
VoltageL1_OBIS,
VoltageL1,
VoltageL2_OBIS,
VoltageL2,
VoltageL3_OBIS,
VoltageL3,
MeterClock_OBIS,
MeterClock,
CumulativeActiveImportEnergy_OBIS,
CumulativeActiveImportEnergy,
CumulativeActiveExportEnergy_OBIS,
CumulativeActiveExportEnergy,
CumulativeReactiveImportEnergy_OBIS,
CumulativeReactiveImportEnergy,
CumulativeReactiveExportEnergy_OBIS,
CumulativeReactiveExportEnergy
};
enum class Kamstrup_List1Phase
{
ListSize,
ListVersionIdentifier,
MeterID_OBIS,
MeterID,
MeterType_OBIS,
MeterType,
ActiveImportPower_OBIS,
ActiveImportPower,
ActiveExportPower_OBIS,
ActiveExportPower,
ReactiveImportPower_OBIS,
ReactiveImportPower,
ReactiveExportPower_OBIS,
ReactiveExportPower,
CurrentL1_OBIS,
CurrentL1,
VoltageL1_OBIS,
VoltageL1,
MeterClock_OBIS,
MeterClock,
CumulativeActiveImportEnergy_OBIS,
CumulativeActiveImportEnergy,
CumulativeActiveExportEnergy_OBIS,
CumulativeActiveExportEnergy,
CumulativeReactiveImportEnergy_OBIS,
CumulativeReactiveImportEnergy,
CumulativeReactiveExportEnergy_OBIS,
CumulativeReactiveExportEnergy
};
#endif

View File

@@ -7,4 +7,4 @@ paragraph=The primary aim of the Timezone library is to convert Universal Coordi
category=Timing
url=https://github.com/JChristensen/Timezone
architectures=*
depends=Time
depends=Time (=1.6.0)

View File

@@ -7,12 +7,11 @@ board = esp12e
framework = ${common.framework}
lib_deps = ${common.lib_deps}
build_flags =
-D HW_ROARFRED=1
-D DEBUG_MODE=1
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
monitor_speed = 2400
monitor_speed = 115200 ; If serial port is shared with HAN, use 2400 and parity E (or N for Norwegian Kamstrup)
monitor_flags =
--parity
E
N

View File

@@ -1,63 +1,32 @@
[platformio]
extra_configs = platformio-user.ini
[common]
lib_deps = file://lib/Timezone, 256dpi/MQTT@2.5.0, OneWireNg@0.10.0, DallasTemperature@3.9.1, EspSoftwareSerial@6.14.1, https://github.com/gskjold/RemoteDebug.git, Time@1.6.0
lib_ignore = OneWire
[env:esp8266]
platform = espressif8266@3.2.0
board = esp12e
board_build.ldscript = eagle.flash.4m2m.ld
framework = arduino
lib_deps = HanReader@1.0.1, ArduinoJson@6.14.1, MQTT@2.4.7, DallasTemperature@3.8.1, EspSoftwareSerial@6.7.1, Base64@1.0.0, RemoteDebug@3.0.5
[env:hw1esp12e]
platform = espressif8266@2.3.3
board = esp12e
framework = ${common.framework}
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
build_flags =
-D HW_ROARFRED=1
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
-D WEBSOCKET_DISABLED=1
[env:esp12e]
platform = espressif8266@2.3.3
board = esp12e
framework = ${common.framework}
lib_deps = ${common.lib_deps}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
[env:d1mini]
platform = espressif8266@2.3.3
board = d1_mini
framework = ${common.framework}
lib_deps = ${common.lib_deps}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
[env:esp32]
platform = espressif32@1.11.2
platform = https://github.com/platformio/platform-espressif32.git#feature/arduino-upstream
board = esp32dev
framework = ${common.framework}
lib_deps = ${common.lib_deps}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
[env:lolind32]
platform = espressif32@1.11.2
board = lolin_d32
framework = ${common.framework}
lib_deps = ${common.lib_deps}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
[env:featheresp32]
platform = espressif32@1.11.2
board = featheresp32
framework = ${common.framework}
framework = arduino
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
build_flags =
-D WEBSOCKET_DISABLED=1

View File

@@ -2,9 +2,34 @@ import os
import re
import shutil
try:
from css_html_js_minify import html_minify, js_minify, css_minify
except:
from SCons.Script import (
ARGUMENTS,
COMMAND_LINE_TARGETS,
DefaultEnvironment,
)
env = DefaultEnvironment()
env.Execute(
env.VerboseAction(
'$PYTHONEXE -m pip install "css_html_js_minify" ',
"Installing Python dependencies",
)
)
try:
from css_html_js_minify import html_minify, js_minify, css_minify
except:
print("WARN: Unable to load minifier")
webroot = "web"
srcroot = "src/web/root"
version = os.environ.get('GITHUB_TAG')
if version == None:
version = "SNAPSHOT"
if os.path.exists(srcroot):
shutil.rmtree(srcroot)
@@ -20,11 +45,28 @@ for filename in os.listdir(webroot):
varname = basename.upper()
with open(srcfile, encoding="utf-8") as f:
content = f.read().replace("${version}", version)
try:
if filename.endswith(".html"):
content = html_minify(content)
elif filename.endswith(".css"):
content = css_minify(content)
elif (filename.endswith(".js") and filename != 'gaugemeter.js') or filename.endswith(".json"):
content = js_minify(content)
except:
print("WARN: Unable to minify")
with open(dstfile, "w") as dst:
dst.write("const char ")
dst.write(varname)
dst.write("[] PROGMEM = R\"==\"==(\n")
with open(srcfile, "r") as src:
for line in src.readlines():
dst.write(line)
dst.write("\n)==\"==\";\n")
dst.write("[] PROGMEM = R\"==\"==(")
dst.write(content)
dst.write(")==\"==\";\n")
dst.write("const int ");
dst.write(varname)
dst.write("_LEN PROGMEM = ");
dst.write(str(len(content)))
dst.write(";");

File diff suppressed because it is too large Load Diff

View File

@@ -3,135 +3,332 @@
#include <EEPROM.h>
#include "Arduino.h"
#define EEPROM_SIZE 1024*3
#define EEPROM_CHECK_SUM 90 // Used to check if config is stored. Change if structure changes
#define EEPROM_CONFIG_ADDRESS 0
#define EEPROM_TEMP_CONFIG_ADDRESS 2048
#define CONFIG_SYSTEM_START 8
#define CONFIG_WIFI_START 16
#define CONFIG_METER_START 224
#define CONFIG_GPIO_START 266
#define CONFIG_ENTSOE_START 286
#define CONFIG_WEB_START 648
#define CONFIG_DEBUG_START 824
#define CONFIG_DOMOTICZ_START 856
#define CONFIG_NTP_START 872
#define CONFIG_MQTT_START 1004
#define CONFIG_MQTT_START_86 224
#define CONFIG_METER_START_87 784
#define CONFIG_GPIO_START_88 832
#define CONFIG_ENTSOE_START_89 944
struct SystemConfig {
uint8_t boardType;
}; // 1
struct WiFiConfig {
char ssid[32];
char psk[64];
char ip[15];
char gateway[15];
char subnet[15];
char dns1[15];
char dns2[15];
char hostname[32];
bool mdns;
}; // 204
struct MqttConfig86 {
char host[128];
uint16_t port;
char clientId[32];
char publishTopic[64];
char subscribeTopic[64];
char username[64];
char password[64];
uint8_t payloadFormat;
bool ssl;
}; // 420
struct MqttConfig {
char host[128];
uint16_t port;
char clientId[32];
char publishTopic[64];
char subscribeTopic[64];
char username[128];
char password[256];
uint8_t payloadFormat;
bool ssl;
}; // 676
struct WebConfig {
uint8_t security;
char username[64];
char password[64];
}; // 129
struct MeterConfig {
uint32_t baud;
uint8_t parity;
bool invert;
uint8_t distributionSystem;
uint8_t mainFuse;
uint8_t productionCapacity;
uint8_t encryptionKey[16];
uint8_t authenticationKey[16];
}; // 41
struct MeterConfig87 {
uint8_t type;
uint8_t distributionSystem;
uint8_t mainFuse;
uint8_t productionCapacity;
uint8_t encryptionKey[16];
uint8_t authenticationKey[16];
bool substituteMissing;
}; // 37
struct DebugConfig {
bool telnet;
bool serial;
uint8_t level;
}; // 3
struct GpioConfig {
uint8_t hanPin;
uint8_t apPin;
uint8_t ledPin;
bool ledInverted;
uint8_t ledPinRed;
uint8_t ledPinGreen;
uint8_t ledPinBlue;
bool ledRgbInverted;
uint8_t tempSensorPin;
uint8_t tempAnalogSensorPin;
uint8_t vccPin;
int16_t vccOffset;
uint16_t vccMultiplier;
uint8_t vccBootLimit;
uint16_t vccResistorGnd;
uint16_t vccResistorVcc;
}; // 20
struct GpioConfig88 {
uint8_t hanPin;
uint8_t apPin;
uint8_t ledPin;
bool ledInverted;
uint8_t ledPinRed;
uint8_t ledPinGreen;
uint8_t ledPinBlue;
bool ledRgbInverted;
uint8_t tempSensorPin;
uint8_t tempAnalogSensorPin;
uint8_t vccPin;
int16_t vccOffset;
uint16_t vccMultiplier;
uint8_t vccBootLimit;
}; // 16
struct DomoticzConfig {
uint16_t elidx;
uint16_t vl1idx;
uint16_t vl2idx;
uint16_t vl3idx;
uint16_t cl1idx;
}; // 10
struct NtpConfig {
bool enable;
bool dhcp;
int16_t offset;
int16_t summerOffset;
char server[64];
}; // 70
struct EntsoeConfig89 {
char token[37];
char area[17];
char currency[4];
uint16_t multiplier;
}; // 60
struct EntsoeConfig {
char token[37];
char area[17];
char currency[4];
uint32_t multiplier;
}; // 62
struct ConfigObject83 {
uint8_t boardType;
char wifiSsid[32];
char wifiPassword[64];
char wifiIp[15];
char wifiGw[15];
char wifiSubnet[15];
char wifiDns1[15];
char wifiDns2[15];
char wifiHostname[32];
char mqttHost[128];
uint16_t mqttPort;
char mqttClientId[32];
char mqttPublishTopic[64];
char mqttSubscribeTopic[64];
char mqttUser[64];
char mqttPassword[64];
uint8_t mqttPayloadFormat;
bool mqttSsl;
uint8_t authSecurity;
char authUser[64];
char authPassword[64];
uint8_t meterType;
uint8_t distributionSystem;
uint8_t mainFuse;
uint8_t productionCapacity;
uint8_t meterEncryptionKey[16];
uint8_t meterAuthenticationKey[16];
bool substituteMissing;
bool sendUnknown;
bool debugTelnet;
bool debugSerial;
uint8_t debugLevel;
uint8_t hanPin;
uint8_t apPin;
uint8_t ledPin;
bool ledInverted;
uint8_t ledPinRed;
uint8_t ledPinGreen;
uint8_t ledPinBlue;
bool ledRgbInverted;
uint8_t tempSensorPin;
uint8_t vccPin;
int16_t vccOffset;
uint16_t vccMultiplier;
uint8_t vccBootLimit;
uint16_t domoELIDX;
uint16_t domoVL1IDX;
uint16_t domoVL2IDX;
uint16_t domoVL3IDX;
uint16_t domoCL1IDX;
bool mDnsEnable;
bool ntpEnable;
bool ntpDhcp;
int16_t ntpOffset;
int16_t ntpSummerOffset;
char ntpServer[64];
uint8_t tempAnalogSensorPin;
};
struct TempSensorConfig {
uint8_t address[8];
char name[16];
bool common;
};
class AmsConfiguration {
public:
bool hasConfig();
int getConfigVersion();
bool load();
bool save();
String getWifiSsid();
void setWifiSsid(String wifiSsid);
String getWifiPassword();
void setWifiPassword(String wifiPassword);
String getWifiIp();
void setWifiIp(String wifiIp);
String getWifiGw();
void setWifiGw(String wifiGw);
String getWifiSubnet();
void setWifiSubnet(String wifiSubnet);
String getWifiDns1();
void setWifiDns1(String wifiDns1);
String getWifiDns2();
void setWifiDns2(String wifiDns1);
String getWifiHostname();
void setWifiHostname(String wifiHostname);
void clearWifiIp();
bool getSystemConfig(SystemConfig&);
bool setSystemConfig(SystemConfig&);
bool getWiFiConfig(WiFiConfig&);
bool setWiFiConfig(WiFiConfig&);
void clearWifi(WiFiConfig&);
void clearWifiIp(WiFiConfig&);
bool isWifiChanged();
void ackWifiChange();
String getMqttHost();
void setMqttHost(String mqttHost);
int getMqttPort();
void setMqttPort(int mqttPort);
String getMqttClientId();
void setMqttClientId(String mqttClientId);
String getMqttPublishTopic();
void setMqttPublishTopic(String mqttPublishTopic);
String getMqttSubscribeTopic();
void setMqttSubscribeTopic(String mqttSubscribeTopic);
String getMqttUser();
void setMqttUser(String mqttUser);
String getMqttPassword();
void setMqttPassword(String mqttPassword);
int getMqttPayloadFormat();
void setMqttPayloadFormat(int mqttPayloadFormat);
void clearMqtt();
bool getMqttConfig(MqttConfig&);
bool setMqttConfig(MqttConfig&);
void clearMqtt(MqttConfig&);
void setMqttChanged();
bool isMqttChanged();
void ackMqttChange();
byte getAuthSecurity();
void setAuthSecurity(byte authSecurity);
String getAuthUser();
void setAuthUser(String authUser);
String getAuthPassword();
void setAuthPassword(String authPassword);
void clearAuth();
bool getWebConfig(WebConfig&);
bool setWebConfig(WebConfig&);
void clearAuth(WebConfig&);
int getMeterType();
void setMeterType(int meterType);
int getDistributionSystem();
void setDistributionSystem(int distributionSystem);
int getMainFuse();
void setMainFuse(int mainFuse);
int getProductionCapacity();
void setProductionCapacity(int productionCapacity);
bool getMeterConfig(MeterConfig&);
bool setMeterConfig(MeterConfig&);
void clearMeter(MeterConfig&);
bool isMeterChanged();
void ackMeterChanged();
bool isDebugTelnet();
void setDebugTelnet(bool debugTelnet);
bool isDebugSerial();
void setDebugSerial(bool debugSerial);
int getDebugLevel();
void setDebugLevel(int debugLevel);
bool getDebugConfig(DebugConfig&);
bool setDebugConfig(DebugConfig&);
void clearDebug(DebugConfig&);
bool pinUsed(uint8_t, GpioConfig&);
bool getGpioConfig(GpioConfig&);
bool setGpioConfig(GpioConfig&);
void clearGpio(GpioConfig&);
void print(Print* debugger);
bool getDomoticzConfig(DomoticzConfig&);
bool setDomoticzConfig(DomoticzConfig&);
void clearDomo(DomoticzConfig&);
bool isDomoChanged();
void ackDomoChange();
bool getNtpConfig(NtpConfig&);
bool setNtpConfig(NtpConfig&);
void clearNtp(NtpConfig&);
bool isNtpChanged();
void ackNtpChange();
bool getEntsoeConfig(EntsoeConfig&);
bool setEntsoeConfig(EntsoeConfig&);
void clearEntsoe(EntsoeConfig&);
bool isEntsoeChanged();
void ackEntsoeChange();
void loadTempSensors();
void saveTempSensors();
uint8_t getTempSensorCount();
TempSensorConfig* getTempSensorConfig(uint8_t address[8]);
void updateTempSensorConfig(uint8_t address[8], const char name[32], bool common);
bool isSensorAddressEqual(uint8_t a[8], uint8_t b[8]);
void clear();
protected:
private:
int configVersion = 0;
uint8_t configVersion = 0;
String wifiSsid;
String wifiPassword;
String wifiIp;
String wifiGw;
String wifiSubnet;
String wifiDns1;
String wifiDns2;
String wifiHostname;
bool wifiChanged = false;
String mqttHost;
int mqttPort = 1883;
String mqttClientId;
String mqttPublishTopic;
String mqttSubscribeTopic;
String mqttUser;
String mqttPassword;
int mqttPayloadFormat = 0;
bool mqttChanged = false;
bool wifiChanged, mqttChanged, meterChanged = true, domoChanged, ntpChanged = true, entsoeChanged = false;
byte authSecurity;
String authUser;
String authPassword;
uint8_t tempSensorCount = 0;
TempSensorConfig** tempSensors;
int meterType = 0, distributionSystem = 0, mainFuse = 0, productionCapacity = 0;
bool loadConfig83(int address);
bool relocateConfig86();
bool relocateConfig87();
bool relocateConfig88(); // dev 1.6
bool relocateConfig89(); // dev 1.6
bool debugTelnet = false, debugSerial = false;
int debugLevel = 3;
const int EEPROM_SIZE = 512;
const int EEPROM_CHECK_SUM = 81; // Used to check if config is stored. Change if structure changes
const int EEPROM_CONFIG_ADDRESS = 0;
bool loadConfig72(int address);
bool loadConfig75(int address);
bool loadConfig80(int address);
bool loadConfig81(int address);
int saveString(int pAddress, const char* pString);
int readString(int pAddress, char* pString[]);
int saveInt(int pAddress, int pValue);
int readInt(int pAddress, int *pValue);
int saveBool(int pAddress, bool pValue);
int readBool(int pAddress, bool *pValue);
int saveByte(int pAddress, byte pValue);
int readByte(int pAddress, byte *pValue);
template <class T> int writeAnything(int ee, const T& value);
template <class T> int readAnything(int ee, T& value);
};
#endif

View File

@@ -1,250 +1,48 @@
#include "AmsData.h"
#include "Kaifa.h"
#include "Aidon.h"
#include "Kamstrup.h"
AmsData::AmsData() {}
AmsData::AmsData(int meterType, HanReader& hanReader) {
lastUpdateMillis = millis();
packageTimestamp = hanReader.getPackageTime();
int listSize = hanReader.getListSize();
switch(meterType) {
case METER_TYPE_KAIFA:
extractFromKaifa(hanReader, listSize);
break;
case METER_TYPE_AIDON:
extractFromAidon(hanReader, listSize);
break;
case METER_TYPE_KAMSTRUP:
extractFromKamstrup(hanReader, listSize);
break;
}
}
void AmsData::extractFromKaifa(HanReader& hanReader, int listSize) {
switch(listSize) {
case (int)Kaifa::List1:
listType = 1;
break;
case (int)Kaifa::List3PhaseShort:
threePhase = true;
case (int)Kaifa::List1PhaseShort:
listType = 2;
break;
case (int)Kaifa::List3PhaseLong:
threePhase = true;
case (int)Kaifa::List1PhaseLong:
listType = 3;
break;
}
if(listSize == (int)Kaifa::List1) {
activeImportPower = hanReader.getInt((int)Kaifa_List1::ActivePowerImported);
} else {
switch(listSize) {
case (int)Kaifa::List3PhaseLong:
meterTimestamp = hanReader.getTime( (int)Kaifa_List3Phase::MeterClock);
activeImportCounter = ((double) hanReader.getInt((int)Kaifa_List3Phase::CumulativeActiveImportEnergy)) / 1000;
activeExportCounter = ((double) hanReader.getInt((int)Kaifa_List3Phase::CumulativeActiveExportEnergy)) / 1000;
reactiveImportCounter = ((double) hanReader.getInt((int)Kaifa_List3Phase::CumulativeReactiveImportEnergy)) / 1000;
reactiveExportCounter = ((double) hanReader.getInt((int)Kaifa_List3Phase::CumulativeReactiveExportEnergy)) / 1000;
case (int)Kaifa::List3PhaseShort:
listId = hanReader.getString( (int)Kaifa_List3Phase::ListVersionIdentifier);
meterId = hanReader.getString( (int)Kaifa_List3Phase::MeterID);
meterType = hanReader.getString( (int)Kaifa_List3Phase::MeterType);
activeImportPower = hanReader.getInt( (int)Kaifa_List3Phase::ActiveImportPower);
reactiveImportPower = hanReader.getInt( (int)Kaifa_List3Phase::ReactiveImportPower);
activeExportPower = hanReader.getInt( (int)Kaifa_List3Phase::ActiveExportPower);
reactiveExportPower = hanReader.getInt( (int)Kaifa_List3Phase::ReactiveExportPower);
l1current = ((double) hanReader.getInt((int)Kaifa_List3Phase::CurrentL1)) / 1000;
l2current = ((double) hanReader.getInt((int)Kaifa_List3Phase::CurrentL2)) / 1000;
l3current = ((double) hanReader.getInt((int)Kaifa_List3Phase::CurrentL3)) / 1000;
l1voltage = ((double) hanReader.getInt((int)Kaifa_List3Phase::VoltageL1)) / 10;
l2voltage = ((double) hanReader.getInt((int)Kaifa_List3Phase::VoltageL2)) / 10;
l3voltage = ((double) hanReader.getInt((int)Kaifa_List3Phase::VoltageL3)) / 10;
break;
case (int)Kaifa::List1PhaseLong:
meterTimestamp = hanReader.getTime( (int)Kaifa_List1Phase::MeterClock);
activeImportCounter = ((double) hanReader.getInt((int)Kaifa_List1Phase::CumulativeActiveImportEnergy));
activeExportCounter = ((double) hanReader.getInt((int)Kaifa_List1Phase::CumulativeActiveExportEnergy));
reactiveImportCounter = ((double) hanReader.getInt((int)Kaifa_List1Phase::CumulativeReactiveImportEnergy));
reactiveExportCounter = ((double) hanReader.getInt((int)Kaifa_List1Phase::CumulativeReactiveExportEnergy));
case (int)Kaifa::List1PhaseShort:
listId = hanReader.getString( (int)Kaifa_List1Phase::ListVersionIdentifier);
meterId = hanReader.getString( (int)Kaifa_List1Phase::MeterID);
meterType = hanReader.getString( (int)Kaifa_List1Phase::MeterType);
activeImportPower = hanReader.getInt( (int)Kaifa_List1Phase::ActiveImportPower);
reactiveImportPower = hanReader.getInt( (int)Kaifa_List1Phase::ReactiveImportPower);
activeExportPower = hanReader.getInt( (int)Kaifa_List1Phase::ActiveExportPower);
reactiveExportPower = hanReader.getInt( (int)Kaifa_List1Phase::ReactiveExportPower);
l1current = ((double) hanReader.getInt((int)Kaifa_List1Phase::CurrentL1)) / 1000;
l1voltage = ((double) hanReader.getInt((int)Kaifa_List1Phase::VoltageL1)) / 10;
break;
}
}
}
void AmsData::extractFromAidon(HanReader& hanReader, int listSize) {
switch(listSize) {
case (int)Aidon::List1:
listType = 1;
break;
case (int)Aidon::List3PhaseITShort:
case (int)Aidon::List3PhaseShort:
threePhase = true;
case (int)Aidon::List1PhaseShort:
listType = 2;
break;
case (int)Aidon::List3PhaseITLong:
case (int)Aidon::List3PhaseLong:
threePhase = true;
case (int)Aidon::List1PhaseLong:
listType = 3;
break;
}
if(listSize == (int)Aidon::List1) {
activeImportPower = hanReader.getInt((int)Aidon_List1::ActiveImportPower);
} else {
switch(listSize) {
case (int)Aidon::List3PhaseLong:
meterTimestamp = hanReader.getTime( (int)Aidon_List3Phase::Timestamp);
activeImportCounter = ((double) hanReader.getInt( (int)Aidon_List3Phase::CumulativeActiveImportEnergy)) / 100;
activeExportCounter = ((double) hanReader.getInt( (int)Aidon_List3Phase::CumulativeActiveExportEnergy)) / 100;
reactiveImportCounter = ((double) hanReader.getInt( (int)Aidon_List3Phase::CumulativeReactiveImportEnergy)) / 100;
reactiveExportCounter = ((double) hanReader.getInt( (int)Aidon_List3Phase::CumulativeReactiveExportEnergy)) / 100;
case (int)Aidon::List3PhaseShort:
listId = hanReader.getString( (int)Aidon_List3Phase::ListVersionIdentifier);
meterId = hanReader.getString( (int)Aidon_List3Phase::MeterID);
meterType = hanReader.getString( (int)Aidon_List3Phase::MeterType);
activeImportPower = hanReader.getInt( (int)Aidon_List3Phase::ActiveImportPower);
reactiveImportPower = hanReader.getInt( (int)Aidon_List3Phase::ReactiveImportPower);
activeExportPower = hanReader.getInt( (int)Aidon_List3Phase::ActiveExportPower);
reactiveExportPower = hanReader.getInt( (int)Aidon_List3Phase::ReactiveExportPower);
l1current = ((double) hanReader.getInt( (int)Aidon_List3Phase::CurrentL1)) / 10;
l2current = ((double) hanReader.getInt( (int)Aidon_List3Phase::CurrentL2)) / 10;
l3current = ((double) hanReader.getInt( (int)Aidon_List3Phase::CurrentL3)) / 10;
l1voltage = ((double) hanReader.getInt( (int)Aidon_List3Phase::VoltageL1)) / 10;
l2voltage = ((double) hanReader.getInt( (int)Aidon_List3Phase::VoltageL2)) / 10;
l3voltage = ((double) hanReader.getInt( (int)Aidon_List3Phase::VoltageL3)) / 10;
break;
case (int)Aidon::List1PhaseLong:
meterTimestamp = hanReader.getTime( (int)Aidon_List1Phase::Timestamp);
activeImportCounter = ((double) hanReader.getInt( (int)Aidon_List1Phase::CumulativeActiveImportEnergy)) / 100;
activeExportCounter = ((double) hanReader.getInt( (int)Aidon_List1Phase::CumulativeActiveExportEnergy)) / 100;
reactiveImportCounter = ((double) hanReader.getInt( (int)Aidon_List1Phase::CumulativeReactiveImportEnergy)) / 100;
reactiveExportCounter = ((double) hanReader.getInt( (int)Aidon_List1Phase::CumulativeReactiveExportEnergy)) / 100;
case (int)Aidon::List1PhaseShort:
listId = hanReader.getString( (int)Aidon_List1Phase::ListVersionIdentifier);
meterId = hanReader.getString( (int)Aidon_List1Phase::MeterID);
meterType = hanReader.getString( (int)Aidon_List1Phase::MeterType);
activeImportPower = hanReader.getInt( (int)Aidon_List1Phase::ActiveImportPower);
reactiveImportPower = hanReader.getInt( (int)Aidon_List1Phase::ReactiveImportPower);
activeExportPower = hanReader.getInt( (int)Aidon_List1Phase::ActiveExportPower);
reactiveExportPower = hanReader.getInt( (int)Aidon_List1Phase::ReactiveExportPower);
l1current = ((double) hanReader.getInt( (int)Aidon_List1Phase::CurrentL1)) / 10;
l1voltage = ((double) hanReader.getInt( (int)Aidon_List1Phase::VoltageL1)) / 10;
break;
case (int)Aidon::List3PhaseITLong:
meterTimestamp = hanReader.getTime( (int)Aidon_List3PhaseIT::Timestamp);
activeImportCounter = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::CumulativeActiveImportEnergy)) / 100;
activeExportCounter = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::CumulativeActiveExportEnergy)) / 100;
reactiveImportCounter = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::CumulativeReactiveImportEnergy)) / 100;
reactiveExportCounter = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::CumulativeReactiveExportEnergy)) / 100;
case (int)Aidon::List3PhaseITShort:
listId = hanReader.getString( (int)Aidon_List3PhaseIT::ListVersionIdentifier);
meterId = hanReader.getString( (int)Aidon_List3PhaseIT::MeterID);
meterType = hanReader.getString( (int)Aidon_List3PhaseIT::MeterType);
activeImportPower = hanReader.getInt( (int)Aidon_List3PhaseIT::ActiveImportPower);
reactiveImportPower = hanReader.getInt( (int)Aidon_List3PhaseIT::ReactiveImportPower);
activeExportPower = hanReader.getInt( (int)Aidon_List3PhaseIT::ActiveExportPower);
reactiveExportPower = hanReader.getInt( (int)Aidon_List3PhaseIT::ReactiveExportPower);
l1current = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::CurrentL1)) / 10;
l2current = 0;
l3current = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::CurrentL3)) / 10;
l1voltage = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::VoltageL1)) / 10;
l2voltage = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::VoltageL2)) / 10;
l3voltage = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::VoltageL3)) / 10;
//l2current = ((activeImportPower * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage;
break;
}
}
}
void AmsData::extractFromKamstrup(HanReader& hanReader, int listSize) {
switch(listSize) {
case (int)Kamstrup::List3PhaseShort:
threePhase = true;
case (int)Kamstrup::List1PhaseShort:
listType = 2;
break;
case (int)Kamstrup::List3PhaseLong:
threePhase = true;
case (int)Kamstrup::List1PhaseLong:
listType = 3;
break;
}
switch(listSize) {
case (int)Kamstrup::List3PhaseLong:
meterTimestamp = hanReader.getTime( (int)Kamstrup_List3Phase::MeterClock);
activeImportCounter = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CumulativeActiveImportEnergy)) / 100;
activeExportCounter = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CumulativeActiveExportEnergy)) / 100;
reactiveImportCounter = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CumulativeReactiveImportEnergy)) / 100;
reactiveExportCounter = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CumulativeReactiveExportEnergy)) / 100;
case (int)Kamstrup::List3PhaseShort:
listId = hanReader.getString( (int)Kamstrup_List3Phase::ListVersionIdentifier);
meterId = hanReader.getString( (int)Kamstrup_List3Phase::MeterID);
meterType = hanReader.getString( (int)Kamstrup_List3Phase::MeterType);
activeImportPower = hanReader.getInt( (int)Kamstrup_List3Phase::ActiveImportPower);
reactiveImportPower = hanReader.getInt( (int)Kamstrup_List3Phase::ReactiveImportPower);
activeExportPower = hanReader.getInt( (int)Kamstrup_List3Phase::ActiveExportPower);
reactiveExportPower = hanReader.getInt( (int)Kamstrup_List3Phase::ReactiveExportPower);
l1current = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CurrentL1)) / 100;
l2current = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CurrentL2)) / 100;
l3current = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CurrentL3)) / 100;
l1voltage = hanReader.getInt( (int)Kamstrup_List3Phase::VoltageL1);
l2voltage = hanReader.getInt( (int)Kamstrup_List3Phase::VoltageL2);
l3voltage = hanReader.getInt( (int)Kamstrup_List3Phase::VoltageL3);
break;
case (int)Kamstrup::List1PhaseLong:
meterTimestamp = hanReader.getTime( (int)Kamstrup_List1Phase::MeterClock);
activeImportCounter = ((double) hanReader.getInt((int)Kamstrup_List1Phase::CumulativeActiveImportEnergy)) / 100;
activeExportCounter = ((double) hanReader.getInt((int)Kamstrup_List1Phase::CumulativeActiveExportEnergy)) / 100;
reactiveImportCounter = ((double) hanReader.getInt((int)Kamstrup_List1Phase::CumulativeReactiveImportEnergy)) / 100;
reactiveExportCounter = ((double) hanReader.getInt((int)Kamstrup_List1Phase::CumulativeReactiveExportEnergy)) / 100;
case (int)Kamstrup::List1PhaseShort:
listId = hanReader.getString( (int)Kamstrup_List1Phase::ListVersionIdentifier);
meterId = hanReader.getString( (int)Kamstrup_List1Phase::MeterID);
meterType = hanReader.getString( (int)Kamstrup_List1Phase::MeterType);
activeImportPower = hanReader.getInt( (int)Kamstrup_List1Phase::ActiveImportPower);
reactiveImportPower = hanReader.getInt( (int)Kamstrup_List1Phase::ReactiveImportPower);
activeExportPower = hanReader.getInt( (int)Kamstrup_List1Phase::ActiveExportPower);
reactiveExportPower = hanReader.getInt( (int)Kamstrup_List1Phase::ReactiveExportPower);
l1current = ((double) hanReader.getInt((int)Kamstrup_List1Phase::CurrentL1)) / 100;
l1voltage = hanReader.getInt( (int)Kamstrup_List1Phase::VoltageL1);
break;
}
}
void AmsData::apply(AmsData& other) {
if(other.getListType() < 3) {
unsigned long ms = this->lastUpdateMillis > other.getLastUpdateMillis() ? 0 : other.getLastUpdateMillis() - this->lastUpdateMillis;
if(ms > 0) {
if(other.getActiveImportPower() > 0)
activeImportCounter += (((float) ms) * other.getActiveImportPower()) / 3600000000;
if(other.getListType() > 1) {
if(other.getActiveExportPower() > 0)
activeExportCounter += (((float) ms*2) * other.getActiveExportPower()) / 3600000000;
if(other.getReactiveImportPower() > 0)
reactiveImportCounter += (((float) ms*2) * other.getReactiveImportPower()) / 3600000000;
if(other.getReactiveExportPower() > 0)
reactiveExportCounter += (((float) ms*2) * other.getReactiveExportPower()) / 3600000000;
}
counterEstimated = true;
}
}
this->lastUpdateMillis = other.getLastUpdateMillis();
this->packageTimestamp = other.getPackageTimestamp();
this->listType = max(this->listType, other.getListType());
if(other.getListType() > this->listType)
this->listType = other.getListType();
switch(other.getListType()) {
case 3:
this->powerFactor = other.getPowerFactor();
this->l1PowerFactor = other.getL1PowerFactor();
this->l2PowerFactor = other.getL2PowerFactor();
this->l3PowerFactor = other.getL3PowerFactor();
this->meterTimestamp = other.getMeterTimestamp();
this->activeImportCounter = other.getActiveImportCounter();
this->activeExportCounter = other.getActiveExportCounter();
this->reactiveImportCounter = other.getReactiveImportCounter();
this->reactiveExportCounter = other.getReactiveExportCounter();
this->counterEstimated = false;
case 2:
this->listId = other.getListId();
this->meterId = other.getMeterId();
this->meterType = other.getMeterType();
this->meterModel = other.getMeterModel();
this->reactiveImportPower = other.getReactiveImportPower();
this->activeExportPower = other.getActiveExportPower();
this->reactiveExportPower = other.getReactiveExportPower();
@@ -255,6 +53,7 @@ void AmsData::apply(AmsData& other) {
this->l2voltage = other.getL2Voltage();
this->l3voltage = other.getL3Voltage();
this->threePhase = other.isThreePhase();
this->twoPhase = other.isTwoPhase();
case 1:
this->activeImportPower = other.getActiveImportPower();
}
@@ -264,11 +63,11 @@ unsigned long AmsData::getLastUpdateMillis() {
return this->lastUpdateMillis;
}
unsigned long AmsData::getPackageTimestamp() {
time_t AmsData::getPackageTimestamp() {
return this->packageTimestamp;
}
int AmsData::getListType() {
uint8_t AmsData::getListType() {
return this->listType;
}
@@ -280,70 +79,94 @@ String AmsData::getMeterId() {
return this->meterId;
}
String AmsData::getMeterType() {
uint8_t AmsData::getMeterType() {
return this->meterType;
}
unsigned long AmsData::getMeterTimestamp() {
String AmsData::getMeterModel() {
return this->meterModel;
}
time_t AmsData::getMeterTimestamp() {
return this->meterTimestamp;
}
int AmsData::getActiveImportPower() {
uint16_t AmsData::getActiveImportPower() {
return this->activeImportPower;
}
int AmsData::getReactiveImportPower() {
uint16_t AmsData::getReactiveImportPower() {
return this->reactiveImportPower;
}
int AmsData::getActiveExportPower() {
uint16_t AmsData::getActiveExportPower() {
return this->activeExportPower;
}
int AmsData::getReactiveExportPower() {
uint16_t AmsData::getReactiveExportPower() {
return this->reactiveExportPower;
}
double AmsData::getL1Voltage() {
float AmsData::getL1Voltage() {
return this->l1voltage;
}
double AmsData::getL2Voltage() {
float AmsData::getL2Voltage() {
return this->l2voltage;
}
double AmsData::getL3Voltage() {
float AmsData::getL3Voltage() {
return this->l3voltage;
}
double AmsData::getL1Current() {
float AmsData::getL1Current() {
return this->l1current;
}
double AmsData::getL2Current() {
float AmsData::getL2Current() {
return this->l2current;
}
double AmsData::getL3Current() {
float AmsData::getL3Current() {
return this->l3current;
}
double AmsData::getActiveImportCounter() {
float AmsData::getPowerFactor() {
return this->powerFactor;
}
float AmsData::getL1PowerFactor() {
return this->l1PowerFactor;
}
float AmsData::getL2PowerFactor() {
return this->l2PowerFactor;
}
float AmsData::getL3PowerFactor() {
return this->l3PowerFactor;
}
float AmsData::getActiveImportCounter() {
return this->activeImportCounter;
}
double AmsData::getReactiveImportCounter() {
float AmsData::getReactiveImportCounter() {
return this->reactiveImportCounter;
}
double AmsData::getActiveExportCounter() {
float AmsData::getActiveExportCounter() {
return this->activeExportCounter;
}
double AmsData::getReactiveExportCounter() {
float AmsData::getReactiveExportCounter() {
return this->reactiveExportCounter;
}
bool AmsData::isThreePhase() {
return this->threePhase;
}
bool AmsData::isTwoPhase() {
return this->twoPhase;
}

View File

@@ -3,65 +3,75 @@
#include "Arduino.h"
#include <Timezone.h>
#include "HanReader.h"
#define METER_TYPE_KAIFA 1
#define METER_TYPE_AIDON 2
#define METER_TYPE_KAMSTRUP 3
enum AmsType {
AmsTypeAutodetect = 0x00,
AmsTypeAidon = 0x01,
AmsTypeKaifa = 0x02,
AmsTypeKamstrup = 0x03,
AmsTypeIskra = 0x08,
AmsTypeLandis = 0x09,
AmsTypeSagemcom = 0x0A,
AmsTypeCustom = 0x88,
AmsTypeUnknown = 0xFF
};
class AmsData {
public:
AmsData();
AmsData(int meterType, HanReader& hanReader);
void apply(AmsData& other);
unsigned long getLastUpdateMillis();
unsigned long getPackageTimestamp();
time_t getPackageTimestamp();
int getListType();
uint8_t getListType();
String getListId();
String getMeterId();
String getMeterType();
uint8_t getMeterType();
String getMeterModel();
unsigned long getMeterTimestamp();
time_t getMeterTimestamp();
int getActiveImportPower();
int getReactiveImportPower();
int getActiveExportPower();
int getReactiveExportPower();
uint16_t getActiveImportPower();
uint16_t getReactiveImportPower();
uint16_t getActiveExportPower();
uint16_t getReactiveExportPower();
double getL1Voltage();
double getL2Voltage();
double getL3Voltage();
float getL1Voltage();
float getL2Voltage();
float getL3Voltage();
double getL1Current();
double getL2Current();
double getL3Current();
float getL1Current();
float getL2Current();
float getL3Current();
double getActiveImportCounter();
double getReactiveImportCounter();
double getActiveExportCounter();
double getReactiveExportCounter();
float getPowerFactor();
float getL1PowerFactor();
float getL2PowerFactor();
float getL3PowerFactor();
float getActiveImportCounter();
float getReactiveImportCounter();
float getActiveExportCounter();
float getReactiveExportCounter();
bool isThreePhase();
bool isTwoPhase();
private:
protected:
unsigned long lastUpdateMillis = 0;
int listType = 0;
unsigned long packageTimestamp = 0;
String listId, meterId, meterType;
unsigned long meterTimestamp = 0;
int activeImportPower = 0, reactiveImportPower = 0, activeExportPower = 0, reactiveExportPower = 0;
double l1voltage = 0, l2voltage = 0, l3voltage = 0, l1current = 0, l2current = 0, l3current = 0;
double activeImportCounter = 0, reactiveImportCounter = 0, activeExportCounter = 0, reactiveExportCounter = 0;
bool threePhase = false;
void extractFromKaifa(HanReader& hanReader, int listSize);
void extractFromAidon(HanReader& hanReader, int listSize);
void extractFromKamstrup(HanReader& hanReader, int listSize);
uint8_t listType = 0, meterType = AmsTypeUnknown;
time_t packageTimestamp = 0;
String listId, meterId, meterModel;
time_t meterTimestamp = 0;
uint16_t activeImportPower = 0, reactiveImportPower = 0, activeExportPower = 0, reactiveExportPower = 0;
float l1voltage = 0, l2voltage = 0, l3voltage = 0, l1current = 0, l2current = 0, l3current = 0;
float powerFactor = 0, l1PowerFactor = 0, l2PowerFactor = 0, l3PowerFactor = 0;
float activeImportCounter = 0, reactiveImportCounter = 0, activeExportCounter = 0, reactiveExportCounter = 0;
bool threePhase = false, twoPhase = false, counterEstimated = false;
};
#endif

315
src/AmsDataStorage.cpp Normal file
View File

@@ -0,0 +1,315 @@
#include "AmsDataStorage.h"
#include <lwip/apps/sntp.h>
#include "EEPROM.h"
#include "LittleFS.h"
#include "AmsStorage.h"
AmsDataStorage::AmsDataStorage(RemoteDebug* debugger) {
day.version = 3;
month.version = 4;
this->debugger = debugger;
}
void AmsDataStorage::setTimezone(Timezone* tz) {
this->tz = tz;
}
bool AmsDataStorage::update(AmsData* data) {
time_t now = time(nullptr);
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Time is: %d\n", now);
}
if(now < EPOCH_2021_01_01) {
if(data->getMeterTimestamp() > 0) {
now = data->getMeterTimestamp();
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Using meter timestamp, which is: %d\n", now);
}
} else if(data->getPackageTimestamp() > 0) {
now = data->getPackageTimestamp();
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Using package timestamp, which is: %d\n", now);
}
}
}
if(now-day.lastMeterReadTime < 3595) {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) It is only %d seconds since last update, ignoring\n", (now-day.lastMeterReadTime));
}
return false;
}
tmElements_t tm, last;
breakTime(now, tm);
if(now > EPOCH_2021_01_01) {
tmElements_t last;
if(day.lastMeterReadTime > EPOCH_2021_01_01) {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Last day update: %d\n", day.lastMeterReadTime);
}
breakTime(day.lastMeterReadTime, last);
for(int i = last.Hour; i < tm.Hour; i++) {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Clearing hour: %d\n", i);
}
setHour(i, 0);
}
}
if(month.lastMeterReadTime > EPOCH_2021_01_01) {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Last month update: %d\n", month.lastMeterReadTime);
}
if(tz != NULL) {
breakTime(tz->toLocal(now), tm);
breakTime(tz->toLocal(month.lastMeterReadTime), last);
} else {
breakTime(now, tm);
breakTime(month.lastMeterReadTime, last);
}
for(int i = last.Day; i < tm.Day; i++) {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Clearing day: %d\n", i);
}
setDay(i, 0);
}
}
}
if(data->getListType() != 3) return false;
else if(tm.Minute > 5) return false;
// Update day plot
if(day.activeImport == 0 || now - day.lastMeterReadTime > 86400) {
day.activeImport = data->getActiveImportCounter() * 1000;
day.activeExport = data->getActiveExportCounter() * 1000;
day.lastMeterReadTime = now;
if(debugger->isActive(RemoteDebug::WARNING)) {
debugger->printf("(AmsDataStorage) Too long since last day update, clearing data\n");
}
for(int i = 0; i<24; i++) {
setHour(i, 0);
}
} else if(now - day.lastMeterReadTime < 4000) {
breakTime(now - 3600, tm);
int16_t val = (((data->getActiveImportCounter() * 1000) - day.activeImport) - ((data->getActiveExportCounter() * 1000) - day.activeExport));
setHour(tm.Hour, val);
if(debugger->isActive(RemoteDebug::INFO)) {
debugger->printf("(AmsDataStorage) Usage for hour %d: %d\n", tm.Hour, val);
}
day.activeImport = data->getActiveImportCounter() * 1000;
day.activeExport = data->getActiveExportCounter() * 1000;
day.lastMeterReadTime = now;
} else {
float mins = (now - day.lastMeterReadTime) / 60.0;
uint16_t im = ((data->getActiveImportCounter() * 1000) - day.activeImport);
uint16_t ex = ((data->getActiveExportCounter() * 1000) - day.activeExport);
float ipm = im / mins;
float epm = ex / mins;
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Since last day update, minutes: %.1f, import: %d (%.2f/min), export: %d (%.2f/min)\n", mins, im, ipm, ex, epm);
}
breakTime(day.lastMeterReadTime, tm);
day.lastMeterReadTime = day.lastMeterReadTime - (tm.Minute * 60) - tm.Second;
breakTime(now, tm);
time_t stopAt = now - (tm.Minute * 60) - tm.Second;
while(day.lastMeterReadTime < stopAt) {
time_t cur = min(day.lastMeterReadTime + 3600, stopAt);
uint8_t minutes = round((cur - day.lastMeterReadTime) / 60.0);
if(minutes < 1) break;
breakTime(day.lastMeterReadTime, last);
float val = ((ipm * minutes) - (epm * minutes));
setHour(last.Hour, val);
if(debugger->isActive(RemoteDebug::INFO)) {
debugger->printf("(AmsDataStorage) Estimated usage for hour %u: %.1f (%lu)\n", last.Hour, val, cur);
}
day.activeImport += ipm * minutes;
day.activeExport += epm * minutes;
day.lastMeterReadTime = cur;
}
}
// Update month plot
if(tz != NULL) {
breakTime(tz->toLocal(now), tm);
} else {
breakTime(now, tm);
}
if(tm.Hour == 0 && now-month.lastMeterReadTime > 86300) {
Serial.printf("\n%d %d %d %d\n", month.version, month.lastMeterReadTime, month.activeImport, month.activeExport);
if(month.activeImport == 0 || now - month.lastMeterReadTime > 2678400) {
month.activeImport = data->getActiveImportCounter() * 1000;
month.activeExport = data->getActiveExportCounter() * 1000;
month.lastMeterReadTime = now;
if(debugger->isActive(RemoteDebug::WARNING)) {
debugger->printf("(AmsDataStorage) Too long since last month update, clearing data\n");
}
for(int i = 0; i<31; i++) {
setDay(i, 0);
}
} else if(now - month.lastMeterReadTime < 87000) {
int32_t val = (month.activeImport == 0 ? 0 : ((data->getActiveImportCounter() * 1000) - month.activeImport) - ((data->getActiveExportCounter() * 1000) - month.activeExport));
if(debugger->isActive(RemoteDebug::INFO)) {
debugger->printf("(AmsDataStorage) Usage for day %d: %d\n", tm.Day, val);
}
time_t yesterday = now - 3600;
breakTime(yesterday, tm);
setDay(tm.Day, val);
month.activeImport = data->getActiveImportCounter() * 1000;
month.activeExport = data->getActiveExportCounter() * 1000;
month.lastMeterReadTime = now;
} else {
float hrs = (now - month.lastMeterReadTime) / 3600.0;
uint16_t im = ((data->getActiveImportCounter() * 1000) - month.activeImport);
uint16_t ex = ((data->getActiveExportCounter() * 1000) - month.activeExport);
float iph = im / hrs;
float eph = ex / hrs;
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Since last month update, hours: %.1f, import: %d (%.2f/hr), export: %d (%.2f/hr)\n", hrs, im, iph, ex, eph);
}
if(tz != NULL) {
breakTime(tz->toLocal(month.lastMeterReadTime), tm);
} else {
breakTime(month.lastMeterReadTime, tm);
}
month.lastMeterReadTime = month.lastMeterReadTime - (tm.Hour * 3600) - (tm.Minute * 60) - tm.Second;
if(tz != NULL) {
breakTime(tz->toLocal(now), tm);
} else {
breakTime(now, tm);
}
time_t stopAt = now - (tm.Hour * 3600) - (tm.Minute * 60) - tm.Second;
while(month.lastMeterReadTime < stopAt) {
time_t cur = min(month.lastMeterReadTime + 86400, stopAt);
uint8_t hours = round((cur - month.lastMeterReadTime) / 3600.0);
if(tz != NULL) {
breakTime(tz->toLocal(month.lastMeterReadTime), last);
} else {
breakTime(month.lastMeterReadTime, last);
}
float val = ((iph * hours) - (eph * hours));
setDay(last.Day, val);
if(debugger->isActive(RemoteDebug::INFO)) {
debugger->printf("(AmsDataStorage) Estimated usage for day %u: %.1f (%lu)\n", last.Day, val, cur);
}
month.activeImport += iph * hours;
month.activeExport += eph * hours;
month.lastMeterReadTime += cur;
}
}
}
return true;
}
void AmsDataStorage::setHour(uint8_t hour, int32_t val) {
if(hour < 0) return;
day.points[hour] = val / 10;
}
int16_t AmsDataStorage::getHour(uint8_t hour) {
if(hour < 0) return 0;
return day.points[hour] * 10;
}
void AmsDataStorage::setDay(uint8_t day, int32_t val) {
if(day < 1) return;
month.points[day-1] = val / 10;
}
int32_t AmsDataStorage::getDay(uint8_t day) {
if(day < 1) return 0;
return (month.points[day-1] * 10);
}
bool AmsDataStorage::load() {
if(!LittleFS.begin()) {
if(debugger->isActive(RemoteDebug::ERROR)) {
debugger->printf("(AmsDataStorage) Unable to load LittleFS\n");
}
return false;
}
bool ret = false;
if(LittleFS.exists(FILE_DAYPLOT)) {
File file = LittleFS.open(FILE_DAYPLOT, "r");
char buf[file.size()];
file.readBytes(buf, file.size());
DayDataPoints* day = (DayDataPoints*) buf;
file.close();
if(day->version == 3) {
memcpy(&this->day, day, sizeof(this->day));
ret = true;
} else {
ret = false;
}
}
if(LittleFS.exists(FILE_MONTHPLOT)) {
File file = LittleFS.open(FILE_MONTHPLOT, "r");
char buf[file.size()];
file.readBytes(buf, file.size());
MonthDataPoints* month = (MonthDataPoints*) buf;
file.close();
if(month->version == 4) {
memcpy(&this->month, month, sizeof(this->month));
ret = true;
} else {
ret = false;
}
}
LittleFS.end();
return ret;
}
bool AmsDataStorage::save() {
if(!LittleFS.begin()) {
if(debugger->isActive(RemoteDebug::ERROR)) {
debugger->printf("(AmsDataStorage) Unable to load LittleFS\n");
}
return false;
}
{
File file = LittleFS.open(FILE_DAYPLOT, "w");
char buf[sizeof(day)];
memcpy(buf, &day, sizeof(day));
for(int i = 0; i < sizeof(day); i++) {
file.write(buf[i]);
}
file.close();
}
{
File file = LittleFS.open(FILE_MONTHPLOT, "w");
char buf[sizeof(month)];
memcpy(buf, &month, sizeof(month));
for(int i = 0; i < sizeof(month); i++) {
file.write(buf[i]);
}
file.close();
}
LittleFS.end();
return true;
}

49
src/AmsDataStorage.h Normal file
View File

@@ -0,0 +1,49 @@
#ifndef _AMSDATASTORAGE_H
#define _AMSDATASTORAGE_H
#include "Arduino.h"
#include "AmsData.h"
#include "RemoteDebug.h"
#include "Timezone.h"
#define EPOCH_2021_01_01 1609459200
struct DayDataPoints {
uint8_t version;
int16_t points[24];
time_t lastMeterReadTime;
uint32_t activeImport;
uint32_t activeExport;
}; // 37 bytes
struct MonthDataPoints {
uint8_t version;
int16_t points[31];
time_t lastMeterReadTime;
uint32_t activeImport;
uint32_t activeExport;
}; // 75 bytes
class AmsDataStorage {
public:
AmsDataStorage(RemoteDebug*);
void setTimezone(Timezone*);
bool update(AmsData*);
int16_t getHour(uint8_t);
int32_t getDay(uint8_t);
bool load();
bool save();
private:
Timezone* tz;
DayDataPoints day = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
MonthDataPoints month = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
RemoteDebug* debugger;
void setHour(uint8_t, int32_t);
void setDay(uint8_t, int32_t);
};
#endif

13
src/AmsStorage.h Normal file
View File

@@ -0,0 +1,13 @@
#ifndef _AMSSTORAGE_H
#define _AMSSTORAGE_H
#define FILE_FIRMWARE "/firmware.bin"
#define FILE_MQTT_CA "/mqtt-ca.pem"
#define FILE_MQTT_CERT "/mqtt-cert.pem"
#define FILE_MQTT_KEY "/mqtt-key.pem"
#define FILE_DAYPLOT "/dayplot.bin"
#define FILE_MONTHPLOT "/monthplot.bin"
#endif

View File

@@ -1,77 +1,24 @@
#ifndef _AMSTOMQTTBRIDGE_H
#define _AMSTOMQTTBRIDGE_H
#define WIFI_CONNECTION_TIMEOUT 30000;
#define WIFI_CONNECTION_TIMEOUT 25000;
#define INVALID_BUTTON_PIN 0xFFFFFFFF
#define MAX_PEM_SIZE 4096
#include <SoftwareSerial.h>
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#elif defined(ESP32)
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <ESPmDNS.h>
#include "SPIFFS.h"
#include "Update.h"
#endif
#define RGB_RED 1
#define RGB_GREEN 2
#define RGB_YELLOW 3
#define RGB_ON 1
#define RGB_OFF 0
// Build settings for custom hardware by Roar Fredriksen
#if HW_ROARFRED
#define LED_PIN 2 // The blue on-board LED of the ESP8266 custom AMS board
#define LED_ACTIVE_HIGH 0
#define AP_BUTTON_PIN 0
HardwareSerial *hanSerial = &Serial;
// Build settings for Wemos Lolin D32
#elif defined(ARDUINO_LOLIN_D32)
#define LED_PIN 5
#define LED_ACTIVE_HIGH 0
#define AP_BUTTON_PIN 4
HardwareSerial *hanSerial = &Serial2;
// Build settings for Wemos D1 mini
#elif defined(ARDUINO_ESP8266_WEMOS_D1MINI)
#define LED_PIN D4
#define LED_ACTIVE_HIGH 0
#define AP_BUTTON_PIN D2
#define SOFTWARE_SERIAL 1
#include <SoftwareSerial.h>
SoftwareSerial *hanSerial = new SoftwareSerial(D1);
// Build settings for Adafruit Feather ESP32
#elif defined(ARDUINO_FEATHER_ESP32)
#define LED_PIN LED_BUILTIN
#define LED_ACTIVE_HIGH 1
#define AP_BUTTON_PIN INVALID_BUTTON_PIN
HardwareSerial *hanSerial = &Serial2;
// Default build for ESP32
#elif defined(ESP32)
#define LED_PIN INVALID_BUTTON_PIN
#define LED_ACTIVE_HIGH 1
#define AP_BUTTON_PIN INVALID_BUTTON_PIN
HardwareSerial *hanSerial = &Serial2;
// Default build settings
#else
#define LED_PIN 2
#define LED_ACTIVE_HIGH 0
#define AP_BUTTON_PIN INVALID_BUTTON_PIN
#define SOFTWARE_SERIAL 1
#include <SoftwareSerial.h>
SoftwareSerial *hanSerial = new SoftwareSerial(5);
#endif
#include "LittleFS.h"
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +0,0 @@
#include "HanToJson.h"
void hanToJson(JsonDocument& json, AmsData& data, HwTools& hw, double temperature) {
json["id"] = WiFi.macAddress();
json["up"] = millis();
json["t"] = data.getPackageTimestamp();
double vcc = hw.getVcc();
if(vcc > 0) {
json["vcc"] = serialized(String(vcc, 3));
}
json["rssi"] = hw.getWifiRssi();
if(temperature != DEVICE_DISCONNECTED_C) {
json["temp"] = serialized(String(temperature, 2));
}
// Add a sub-structure to the json object,
// to keep the data from the meter itself
JsonObject jd = json.createNestedObject("data");
switch(data.getListType()) {
case 3:
jd["rtc"] = data.getMeterTimestamp();
jd["tPI"] = data.getActiveImportCounter();
jd["tPO"] = data.getActiveExportCounter();
jd["tQI"] = data.getReactiveImportCounter();
jd["tQO"] = data.getReactiveExportCounter();
case 2:
jd["lv"] = data.getListId();
jd["id"] = data.getMeterId();
jd["type"] = data.getMeterType();
jd["Q"] = data.getReactiveImportPower();
jd["PO"] = data.getActiveExportPower();
jd["QO"] = data.getReactiveExportPower();
jd["I1"] = data.getL1Current();
jd["I2"] = data.getL2Current();
jd["I3"] = data.getL3Current();
jd["U1"] = data.getL1Voltage();
jd["U2"] = data.getL2Voltage();
jd["U3"] = data.getL3Voltage();
case 1:
jd["P"] = data.getActiveImportPower();
}
}

View File

@@ -1,16 +0,0 @@
#ifndef _HANTOJSON_h
#define _HANTOJSON_h
#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif
#include <ArduinoJson.h>
#include "AmsData.h"
#include "HwTools.h"
void hanToJson(JsonDocument& json, AmsData& data, HwTools& hw, double temperature);
#endif

View File

@@ -1,40 +1,399 @@
#include "HwTools.h"
double HwTools::getVcc() {
#if defined(ARDUINO_ESP8266_WEMOS_D1MINI)
return (((double) ESP.getVcc()) / 900); // This board has a voltage divider on VCC. Yes, 900 is correct
#elif defined(ESP8266)
#if defined(ESP_VCC_CALIB_FACTOR)
return ((double) ESP.getVcc()) / 1024 * ESP_VCC_CALIB_FACTOR;
#else
return ((double) ESP.getVcc()) / 1024;
void HwTools::setup(GpioConfig* config, AmsConfiguration* amsConf) {
this->config = config;
this->amsConf = amsConf;
this->tempSensorInit = false;
if(sensorApi != NULL)
delete sensorApi;
if(oneWire != NULL)
delete oneWire;
if(config->tempSensorPin > 0 && config->tempSensorPin < 40) {
pinMode(config->tempSensorPin, INPUT);
} else {
config->tempSensorPin = 0xFF;
}
if(config->vccPin > 0 && config->vccPin < 40) {
getAdcChannel(config->vccPin, voltAdc);
if(voltAdc.unit != 0xFF) {
#if defined(ESP32)
if(voltAdc.unit == ADC_UNIT_1) {
voltAdcChar = (esp_adc_cal_characteristics_t*) calloc(1, sizeof(esp_adc_cal_characteristics_t));
esp_adc_cal_value_t adcVal = esp_adc_cal_characterize((adc_unit_t) voltAdc.unit, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, 1100, voltAdcChar);
adc1_config_channel_atten((adc1_channel_t) voltAdc.channel, ADC_ATTEN_DB_6);
} else if(voltAdc.unit == ADC_UNIT_2) {
voltAdcChar = (esp_adc_cal_characteristics_t*) calloc(1, sizeof(esp_adc_cal_characteristics_t));
esp_adc_cal_value_t adcVal = esp_adc_cal_characterize((adc_unit_t) voltAdc.unit, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, 1100, voltAdcChar);
adc2_config_channel_atten((adc2_channel_t) voltAdc.channel, ADC_ATTEN_DB_6);
}
#endif
} else {
pinMode(config->vccPin, INPUT);
}
} else {
voltAdc.unit = 0xFF;
voltAdc.channel = 0xFF;
config->vccPin = 0xFF;
}
if(config->tempAnalogSensorPin > 0 && config->tempAnalogSensorPin < 40) {
pinMode(config->tempAnalogSensorPin, INPUT);
} else {
config->tempAnalogSensorPin = 0xFF;
}
if(config->ledPin > 0 && config->ledPin < 40) {
pinMode(config->ledPin, OUTPUT);
ledOff(LED_INTERNAL);
} else {
config->ledPin = 0xFF;
}
if(config->ledPinRed > 0 && config->ledPinRed < 40) {
pinMode(config->ledPinRed, OUTPUT);
ledOff(LED_RED);
} else {
config->ledPinRed = 0xFF;
}
if(config->ledPinGreen > 0 && config->ledPinGreen < 40) {
pinMode(config->ledPinGreen, OUTPUT);
ledOff(LED_GREEN);
} else {
config->ledPinGreen = 0xFF;
}
if(config->ledPinBlue > 0 && config->ledPinBlue < 40) {
pinMode(config->ledPinBlue, OUTPUT);
ledOff(LED_BLUE);
} else {
config->ledPinBlue = 0xFF;
}
}
void HwTools::getAdcChannel(uint8_t pin, AdcConfig& config) {
config.unit = 0xFF;
config.channel = 0xFF;
#if defined(ESP32)
switch(pin) {
case ADC1_CHANNEL_0_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_0;
break;
case ADC1_CHANNEL_1_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_1;
break;
case ADC1_CHANNEL_2_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_2;
break;
case ADC1_CHANNEL_3_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_3;
break;
case ADC1_CHANNEL_4_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_4;
break;
case ADC1_CHANNEL_5_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_5;
break;
case ADC1_CHANNEL_6_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_6;
break;
case ADC1_CHANNEL_7_GPIO_NUM:
config.unit = ADC_UNIT_1;
config.channel = ADC1_CHANNEL_7;
break;
case ADC2_CHANNEL_0_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_0;
break;
case ADC2_CHANNEL_1_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_1;
break;
case ADC2_CHANNEL_2_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_2;
break;
case ADC2_CHANNEL_3_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_3;
break;
case ADC2_CHANNEL_4_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_4;
break;
case ADC2_CHANNEL_5_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_5;
break;
case ADC2_CHANNEL_6_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_6;
break;
case ADC2_CHANNEL_7_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_7;
break;
case ADC2_CHANNEL_8_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_8;
break;
case ADC2_CHANNEL_9_GPIO_NUM:
config.unit = ADC_UNIT_2;
config.channel = ADC2_CHANNEL_9;
break;
}
#endif
#endif
return -1;
}
double HwTools::getVcc() {
double volts = 0.0;
if(config->vccPin != 0xFF) {
#if defined(ESP8266)
uint32_t x = 0;
for (int i = 0; i < 10; i++) {
x += analogRead(config->vccPin);
}
volts = x / 10240;
#elif defined(ESP32)
if(voltAdc.unit != 0xFF) {
uint32_t x = 0;
for (int i = 0; i < 10; i++) {
if(voltAdc.unit == ADC_UNIT_1) {
x += adc1_get_raw((adc1_channel_t) voltAdc.channel);
} else if(voltAdc.unit == ADC_UNIT_2) {
int v = 0;
adc2_get_raw((adc2_channel_t) voltAdc.channel, ADC_WIDTH_BIT_12, &v);
x += v;
}
}
x = x / 10;
uint32_t voltage = esp_adc_cal_raw_to_voltage(x, voltAdcChar);
volts = voltage / 1000.0;
} else {
uint32_t x = 0;
for (int i = 0; i < 10; i++) {
x += analogRead(config->vccPin);
}
volts = x / 40950;
}
#endif
} else {
#if defined(ESP8266)
volts = ESP.getVcc() / 1024.0;
#endif
}
if(config->vccResistorGnd > 0 && config->vccResistorVcc > 0) {
volts *= ((double) (config->vccResistorGnd + config->vccResistorVcc) / config->vccResistorGnd);
}
float vccOffset = config->vccOffset / 100.0;
float vccMultiplier = config->vccMultiplier / 1000.0;
return vccOffset + (volts > 0.0 ? volts * vccMultiplier : 0.0);
}
uint8_t HwTools::getTempSensorCount() {
return sensorCount;
}
TempSensorData* HwTools::getTempSensorData(uint8_t i) {
if(i < sensorCount) {
return tempSensors[i];
}
return NULL;
}
bool HwTools::updateTemperatures() {
if(config->tempSensorPin != 0xFF) {
if(!tempSensorInit) {
oneWire = new OneWire(config->tempSensorPin);
sensorApi = new DallasTemperature(this->oneWire);
sensorApi->begin();
delay(100);
tempSensorInit = true;
DeviceAddress addr;
sensorApi->requestTemperatures();
int c = sensorApi->getDeviceCount();
if(this->tempSensors != NULL) {
delete this->tempSensors;
}
this->tempSensors = new TempSensorData*[c];
for(int i = 0; i < c; i++) {
bool found = false;
sensorApi->getAddress(addr, i);
float t = sensorApi->getTempC(addr);
for(int x = 0; x < sensorCount; x++) {
TempSensorData *data = tempSensors[x];
if(isSensorAddressEqual(data->address, addr)) {
found = true;
data->lastRead = t;
if(t > -85) {
data->changed = data->lastValidRead != t;
data->lastValidRead = t;
}
}
}
if(!found) {
TempSensorData *data = new TempSensorData();
memcpy(data->address, addr, 8);
data->lastRead = t;
if(t > -85) {
data->changed = data->lastValidRead != t;
data->lastValidRead = t;
}
tempSensors[sensorCount++] = data;
}
delay(10);
}
} else {
if(sensorCount > 0) {
sensorApi->requestTemperatures();
for(int x = 0; x < sensorCount; x++) {
TempSensorData *data = tempSensors[x];
float t = sensorApi->getTempC(data->address);
data->lastRead = t;
if(t > -85) {
data->changed = data->lastValidRead != t;
data->lastValidRead = t;
}
}
}
}
return true;
}
return false;
}
bool HwTools::isSensorAddressEqual(uint8_t a[8], uint8_t b[8]) {
for(int i = 0; i < 8; i++) {
if(a[i] != b[i]) return false;
}
return true;
}
double HwTools::getTemperature() {
#if defined TEMP_SENSOR_PIN
if(!tempSensorInit) {
tempSensor->begin();
delay(50);
tempSensor->requestTemperatures();
hasTempSensor = tempSensor->getTempCByIndex(0) != DEVICE_DISCONNECTED_C;
tempSensorInit = true;
uint8_t c = 0;
double ret = 0;
double analogTemp = getTemperatureAnalog();
if(analogTemp != DEVICE_DISCONNECTED_C) {
ret += analogTemp;
c++;
}
if(hasTempSensor) {
tempSensor->requestTemperatures();
return tempSensor->getTempCByIndex(0);
} else {
return DEVICE_DISCONNECTED_C;
for(int x = 0; x < sensorCount; x++) {
TempSensorData data = *tempSensors[x];
TempSensorConfig* conf = amsConf->getTempSensorConfig(data.address);
if((conf == NULL || conf->common) && data.lastValidRead > -85) {
ret += data.lastValidRead;
c++;
}
}
return c == 0 ? DEVICE_DISCONNECTED_C : ret/c;
}
double HwTools::getTemperatureAnalog() {
if(config->tempAnalogSensorPin != 0xFF) {
float adcCalibrationFactor = 1.06587;
int volts;
#if defined(ESP8266)
volts = (analogRead(config->tempAnalogSensorPin) / 1024.0) * 3.3;
#elif defined(ESP32)
volts = (analogRead(config->tempAnalogSensorPin) / 4095.0) * 3.3;
#endif
return ((volts * adcCalibrationFactor) - 0.4) / 0.0195;
}
#endif
return DEVICE_DISCONNECTED_C;
}
int HwTools::getWifiRssi() {
int rssi = WiFi.RSSI();
return isnan(rssi) ? -100.0 : rssi;
}
}
bool HwTools::ledOn(uint8_t color) {
if(color == LED_INTERNAL) {
return writeLedPin(color, config->ledInverted ? LOW : HIGH);
} else {
return writeLedPin(color, config->ledRgbInverted ? LOW : HIGH);
}
}
bool HwTools::ledOff(uint8_t color) {
if(color == LED_INTERNAL) {
return writeLedPin(color, config->ledInverted ? HIGH : LOW);
} else {
return writeLedPin(color, config->ledRgbInverted ? HIGH : LOW);
}
}
bool HwTools::ledBlink(uint8_t color, uint8_t blink) {
for(int i = 0; i < blink; i++) {
if(!ledOn(color)) return false;
delay(50);
ledOff(color);
if(i != blink)
delay(50);
}
return true;
}
bool HwTools::writeLedPin(uint8_t color, uint8_t state) {
switch(color) {
case LED_INTERNAL: {
if(config->ledPin != 0xFF) {
digitalWrite(config->ledPin, state);
return true;
} else {
return false;
}
break;
}
case LED_RED: {
if(config->ledPinRed != 0xFF) {
digitalWrite(config->ledPinRed, state);
return true;
} else {
return false;
}
break;
}
case LED_GREEN: {
if(config->ledPinGreen != 0xFF) {
digitalWrite(config->ledPinGreen, state);
return true;
} else {
return false;
}
break;
}
case LED_BLUE: {
if(config->ledPinBlue != 0xFF) {
digitalWrite(config->ledPinBlue, state);
return true;
} else {
return false;
}
break;
}
case LED_YELLOW: {
if(config->ledPinRed != 0xFF && config->ledPinGreen != 0xFF) {
digitalWrite(config->ledPinRed, state);
digitalWrite(config->ledPinGreen, state);
return true;
} else {
return false;
}
break;
}
}
return false;
}

View File

@@ -7,37 +7,65 @@
#include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#include <driver/adc.h>
#include <esp_adc_cal.h>
#include <soc/adc_channel.h>
#endif
#include <DallasTemperature.h>
#include <OneWire.h>
#include "AmsConfiguration.h"
#if HW_ROARFRED
#define TEMP_SENSOR_PIN 5
#elif defined(ARDUINO_LOLIN_D32)
#define TEMP_SENSOR_PIN 14
#elif defined(ARDUINO_ESP8266_WEMOS_D1MINI)
#define TEMP_SENSOR_PIN D5
#else
#define TEMP_SENSOR_PIN 0xFFFFFFFF
#endif
#define LED_INTERNAL 0
#define LED_RED 1
#define LED_GREEN 2
#define LED_BLUE 3
#define LED_YELLOW 4
struct TempSensorData {
uint8_t address[8];
float lastRead;
float lastValidRead;
bool changed;
};
struct AdcConfig {
uint8_t unit;
uint8_t channel;
};
class HwTools {
public:
void setup(GpioConfig*, AmsConfiguration*);
double getVcc();
uint8_t getTempSensorCount();
TempSensorData* getTempSensorData(uint8_t);
bool updateTemperatures();
double getTemperature();
double getTemperatureAnalog();
double getTemperature(uint8_t address[8]);
int getWifiRssi();
bool ledOn(uint8_t color);
bool ledOff(uint8_t color);
bool ledBlink(uint8_t color, uint8_t blink);
HwTools() {
oneWire = new OneWire(TEMP_SENSOR_PIN);
tempSensor = new DallasTemperature(this->oneWire);
};
HwTools() {};
private:
bool tempSensorInit, hasTempSensor;
OneWire *oneWire;
DallasTemperature *tempSensor;
AdcConfig voltAdc, tempAdc;
#if defined(ESP32)
esp_adc_cal_characteristics_t* voltAdcChar, tempAdcChar;
#endif
GpioConfig* config;
AmsConfiguration* amsConf;
bool tempSensorInit;
OneWire *oneWire = NULL;
DallasTemperature *sensorApi = NULL;
uint8_t sensorCount = 0;
TempSensorData** tempSensors = NULL;
bool writeLedPin(uint8_t color, uint8_t state);
bool isSensorAddressEqual(uint8_t a[8], uint8_t b[8]);
void getAdcChannel(uint8_t pin, AdcConfig&);
};
#endif

108
src/IEC6205621.cpp Normal file
View File

@@ -0,0 +1,108 @@
#include "IEC6205621.h"
IEC6205621::IEC6205621(String payload) {
if(payload.length() < 16)
return;
lastUpdateMillis = millis();
listId = payload.substring(payload.startsWith("/") ? 1 : 0, payload.indexOf("\n"));
if(listId.startsWith("ADN")) {
meterType == AmsTypeAidon;
listId = listId.substring(0,4);
} else if(listId.startsWith("KFM")) {
meterType = AmsTypeKaifa;
listId = listId.substring(0,4);
} else if(listId.startsWith("KMP")) {
meterType = AmsTypeKamstrup;
listId = listId.substring(0,4);
} else if(listId.startsWith("ISk")) {
meterType = AmsTypeIskra;
listId = listId.substring(0,5);
} else if(listId.startsWith("XMX")) {
meterType = AmsTypeLandis;
listId = listId.substring(0,6);
} else if(listId.startsWith("Ene")) {
meterType = AmsTypeSagemcom;
listId = listId.substring(0,4);
} else {
meterType = AmsTypeUnknown;
listId = listId.substring(0,4);
}
meterId = extract(payload, "96.1.0");
if(meterId.isEmpty()) {
meterId = extract(payload, "0.0.5");
}
meterModel = extract(payload, "96.1.1");
if(meterModel.isEmpty()) {
meterModel = extract(payload, "96.1.7");
if(meterModel.isEmpty()) {
meterModel = payload.substring(payload.indexOf(listId) + listId.length(), payload.indexOf("\n"));
meterModel.trim();
}
}
String timestamp = extract(payload, "1.0.0");
if(timestamp.length() > 10) {
tmElements_t tm;
tm.Year = (timestamp.substring(0,2).toInt() + 2000) - 1970;
tm.Month = timestamp.substring(4,6).toInt();
tm.Day = timestamp.substring(2,4).toInt();
tm.Hour = timestamp.substring(6,8).toInt();
tm.Minute = timestamp.substring(8,10).toInt();
tm.Second = timestamp.substring(10,12).toInt();
meterTimestamp = makeTime(tm); // TODO: Adjust for time zone
}
activeImportPower = (uint16_t) (extractDouble(payload, "1.7.0") * 1000);
activeExportPower = (uint16_t) (extractDouble(payload, "2.7.0") * 1000);
reactiveImportPower = (uint16_t) (extractDouble(payload, "3.7.0") * 1000);
reactiveExportPower = (uint16_t) (extractDouble(payload, "4.7.0") * 1000);
if(activeImportPower > 0)
listType = 1;
l1voltage = extractDouble(payload, "32.7.0");
l2voltage = extractDouble(payload, "52.7.0");
l3voltage = extractDouble(payload, "72.7.0");
l1current = extractDouble(payload, "31.7.0");
l2current = extractDouble(payload, "51.7.0");
l3current = extractDouble(payload, "71.7.0");
if(l1voltage > 0 || l2voltage > 0 || l3voltage > 0)
listType = 2;
activeImportCounter = extractDouble(payload, "1.8.0");
activeExportCounter = extractDouble(payload, "2.8.0");
reactiveImportCounter = extractDouble(payload, "3.8.0");
reactiveExportCounter = extractDouble(payload, "4.8.0");
if(activeImportCounter > 0 || activeExportCounter > 0 || reactiveImportCounter > 0 || reactiveExportCounter > 0)
listType = 3;
threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0;
twoPhase = (l1voltage > 0 && l2voltage > 0) || (l2voltage > 0 && l3voltage > 0) || (l3voltage > 0 && l1voltage > 0);
if(threePhase) {
if(l2current == 0 && l1current != 0 && l3current != 0) {
l2current = (((activeImportPower - activeExportPower) * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage;
}
}
}
String IEC6205621::extract(String payload, String obis) {
int a = payload.indexOf(String(":" + obis + "("));
if(a > 0) {
int b = payload.indexOf(")", a);
if(b > a) {
return payload.substring(a+obis.length()+2, b);
}
}
return "";
}
double IEC6205621::extractDouble(String payload, String obis) {
return extract(payload, obis).toDouble();
}

14
src/IEC6205621.h Normal file
View File

@@ -0,0 +1,14 @@
#ifndef _IEC62056_21_H
#define _IEC62056_21_H
#include "AmsData.h"
class IEC6205621 : public AmsData {
public:
IEC6205621(String payload);
private:
String extract(String payload, String obis);
double extractDouble(String payload, String obis);
};
#endif

468
src/IEC6205675.cpp Normal file
View File

@@ -0,0 +1,468 @@
#include "IEC6205675.h"
#include "lwip/def.h"
#include "Timezone.h"
IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, CosemDateTime packageTimestamp) {
uint32_t u32;
int32_t s32;
char str[64];
this->packageTimestamp = getTimestamp(packageTimestamp);
u32 = getUnsignedNumber(AMS_OBIS_ACTIVE_IMPORT, sizeof(AMS_OBIS_ACTIVE_IMPORT), ((char *) (d)));
if(u32 == 0xFFFFFFFF) {
CosemData* data = getCosemDataAt(1, ((char *) (d)));
// Kaifa special case...
if(data->base.type == CosemTypeOctetString) {
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
String listId = String(str);
if(listId.startsWith("KFM_001")) {
this->listId = listId;
meterType = AmsTypeKaifa;
int idx = 0;
data = getCosemDataAt(idx, ((char *) (d)));
idx+=2;
if(data->base.length == 0x0D || data->base.length == 0x12) {
listType = data->base.length == 0x12 ? 3 : 2;
data = getCosemDataAt(idx++, ((char *) (d)));
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
meterId = String(str);
data = getCosemDataAt(idx++, ((char *) (d)));
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
meterModel = String(str);
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportPower = ntohl(data->dlu.data);
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportPower = ntohl(data->dlu.data);
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveImportPower = ntohl(data->dlu.data);
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveExportPower = ntohl(data->dlu.data);
data = getCosemDataAt(idx++, ((char *) (d)));
l1current = ntohl(data->dlu.data) / 1000.0;
data = getCosemDataAt(idx++, ((char *) (d)));
l2current = ntohl(data->dlu.data) / 1000.0;
data = getCosemDataAt(idx++, ((char *) (d)));
l3current = ntohl(data->dlu.data) / 1000.0;
data = getCosemDataAt(idx++, ((char *) (d)));
l1voltage = ntohl(data->dlu.data) / 10.0;
data = getCosemDataAt(idx++, ((char *) (d)));
l2voltage = ntohl(data->dlu.data) / 10.0;
data = getCosemDataAt(idx++, ((char *) (d)));
l3voltage = ntohl(data->dlu.data) / 10.0;
} else if(data->base.length == 0x09 || data->base.length == 0x0E) {
listType = data->base.length == 0x0E ? 3 : 2;
data = getCosemDataAt(idx++, ((char *) (d)));
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
meterId = String(str);
data = getCosemDataAt(idx++, ((char *) (d)));
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
meterModel = String(str);
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportPower = ntohl(data->dlu.data);
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportPower = ntohl(data->dlu.data);
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveImportPower = ntohl(data->dlu.data);
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveExportPower = ntohl(data->dlu.data);
data = getCosemDataAt(idx++, ((char *) (d)));
l1current = ntohl(data->dlu.data) / 1000.0;
data = getCosemDataAt(idx++, ((char *) (d)));
l1voltage = ntohl(data->dlu.data) / 10.0;
}
if(listType == 3) {
data = getCosemDataAt(idx++, ((char *) (d)));
switch(data->base.type) {
case CosemTypeOctetString: {
if(data->oct.length == 0x0C) {
AmsOctetTimestamp* ts = (AmsOctetTimestamp*) data;
meterTimestamp = getTimestamp(ts->dt);
}
}
}
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveImportCounter = ntohl(data->dlu.data) / 1000.0;
data = getCosemDataAt(idx++, ((char *) (d)));
reactiveExportCounter = ntohl(data->dlu.data) / 1000.0;
}
lastUpdateMillis = millis();
}
} else if(useMeterType == AmsTypeKaifa && data->base.type == CosemTypeDLongUnsigned) {
listType = 1;
meterType = AmsTypeKaifa;
activeImportPower = ntohl(data->dlu.data);
lastUpdateMillis = millis();
}
// Kaifa end
} else {
listType = 1;
activeImportPower = u32;
meterType = AmsTypeUnknown;
CosemData* version = findObis(AMS_OBIS_VERSION, sizeof(AMS_OBIS_VERSION), d);
if(version != NULL && version->base.type == CosemTypeString) {
if(memcmp(version->str.data, "AIDON", 5) == 0) {
meterType = AmsTypeAidon;
} else if(memcmp(version->str.data, "Kamstrup", 8) == 0) {
meterType = AmsTypeKamstrup;
}
} else {
version = getCosemDataAt(1, ((char *) (d)));
if(version->base.type == CosemTypeString) {
if(memcmp(version->str.data, "Kamstrup", 8) == 0) {
meterType = AmsTypeKamstrup;
}
}
}
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
Timezone tz(CEST, CET);
if(meterType == AmsTypeKamstrup || meterType == AmsTypeAidon) {
this->packageTimestamp = this->packageTimestamp > 0 ? tz.toUTC(this->packageTimestamp) : 0;
}
u32 = getString(AMS_OBIS_VERSION, sizeof(AMS_OBIS_VERSION), ((char *) (d)), str);
if(u32 > 0) {
listId = String(str);
}
u32 = getUnsignedNumber(AMS_OBIS_ACTIVE_EXPORT, sizeof(AMS_OBIS_ACTIVE_EXPORT), ((char *) (d)));
if(u32 != 0xFFFFFFFF) {
activeExportPower = u32;
}
u32 = getUnsignedNumber(AMS_OBIS_REACTIVE_IMPORT, sizeof(AMS_OBIS_REACTIVE_IMPORT), ((char *) (d)));
if(u32 != 0xFFFFFFFF) {
reactiveImportPower = u32;
}
u32 = getUnsignedNumber(AMS_OBIS_REACTIVE_EXPORT, sizeof(AMS_OBIS_REACTIVE_EXPORT), ((char *) (d)));
if(u32 != 0xFFFFFFFF) {
reactiveExportPower = u32;
}
u32 = getUnsignedNumber(AMS_OBIS_VOLTAGE_L1, sizeof(AMS_OBIS_VOLTAGE_L1), ((char *) (d)));
if(u32 != 0xFFFFFFFF) {
listType = 2;
l1voltage = u32;
}
u32 = getUnsignedNumber(AMS_OBIS_VOLTAGE_L2, sizeof(AMS_OBIS_VOLTAGE_L2), ((char *) (d)));
if(u32 != 0xFFFFFFFF) {
listType = 2;
l2voltage = u32;
}
u32 = getUnsignedNumber(AMS_OBIS_VOLTAGE_L3, sizeof(AMS_OBIS_VOLTAGE_L3), ((char *) (d)));
if(u32 != 0xFFFFFFFF) {
listType = 2;
l3voltage = u32;
}
s32 = getSignedNumber(AMS_OBIS_CURRENT_L1, sizeof(AMS_OBIS_CURRENT_L1), ((char *) (d)));
if(s32 != 0xFFFFFFFF) {
listType = 2;
l1current = s32;
}
s32 = getSignedNumber(AMS_OBIS_CURRENT_L2, sizeof(AMS_OBIS_CURRENT_L2), ((char *) (d)));
if(s32 != 0xFFFFFFFF) {
listType = 2;
l2current = s32;
}
s32 = getSignedNumber(AMS_OBIS_CURRENT_L3, sizeof(AMS_OBIS_CURRENT_L3), ((char *) (d)));
if(s32 != 0xFFFFFFFF) {
listType = 2;
l3current = s32;
}
if(listType == 2) {
int vdiv = 1;
int voltage = l1voltage == 0 ? l2voltage == 0 ? l3voltage == 0 ? 0 : l3voltage : l2voltage : l1voltage;
while(voltage > 1000) {
vdiv *= 10;
voltage /= 10;
}
l1voltage = l1voltage != 0 ? l1voltage / vdiv : 0;
l2voltage = l2voltage != 0 ? l2voltage / vdiv : 0;
l3voltage = l3voltage != 0 ? l3voltage / vdiv : 0;
int adiv = 1;
int watt = (l1voltage * l1current) + (l2voltage * l2current) + (l3voltage * l3current);
while(activeImportPower > 0 && watt / activeImportPower > 2) {
adiv *= 10;
watt /= 10;
}
l1current = l1current != 0 ? l1current / adiv : 0;
l2current = l2current != 0 ? l2current / adiv : 0;
l3current = l3current != 0 ? l3current / adiv : 0;
}
u32 = getUnsignedNumber(AMS_OBIS_ACTIVE_IMPORT_COUNT, sizeof(AMS_OBIS_ACTIVE_IMPORT_COUNT), ((char *) (d)));
if(u32 != 0xFFFFFFFF) {
listType = 3;
activeImportCounter = u32 / 100.0;
}
u32 = getUnsignedNumber(AMS_OBIS_ACTIVE_EXPORT_COUNT, sizeof(AMS_OBIS_ACTIVE_EXPORT_COUNT), ((char *) (d)));
if(u32 != 0xFFFFFFFF) {
listType = 3;
activeExportCounter = u32 / 100.0;
}
u32 = getUnsignedNumber(AMS_OBIS_REACTIVE_IMPORT_COUNT, sizeof(AMS_OBIS_REACTIVE_IMPORT_COUNT), ((char *) (d)));
if(u32 != 0xFFFFFFFF) {
listType = 3;
reactiveImportCounter = u32 / 100.0;
}
u32 = getUnsignedNumber(AMS_OBIS_REACTIVE_EXPORT_COUNT, sizeof(AMS_OBIS_REACTIVE_EXPORT_COUNT), ((char *) (d)));
if(u32 != 0xFFFFFFFF) {
listType = 3;
reactiveExportCounter = u32 / 100.0;
}
u32 = getString(AMS_OBIS_METER_MODEL, sizeof(AMS_OBIS_METER_MODEL), ((char *) (d)), str);
if(u32 > 0) {
meterModel = String(str);
} else {
u32 = getString(AMS_OBIS_METER_MODEL_2, sizeof(AMS_OBIS_METER_MODEL_2), ((char *) (d)), str);
if(u32 > 0) {
meterModel = String(str);
}
}
u32 = getString(AMS_OBIS_METER_ID, sizeof(AMS_OBIS_METER_ID), ((char *) (d)), str);
if(u32 > 0) {
meterId = String(str);
} else {
u32 = getString(AMS_OBIS_METER_ID_2, sizeof(AMS_OBIS_METER_ID_2), ((char *) (d)), str);
if(u32 > 0) {
meterId = String(str);
}
}
CosemData* meterTs = findObis(AMS_OBIS_METER_TIMESTAMP, sizeof(AMS_OBIS_METER_TIMESTAMP), ((char *) (d)));
if(meterTs != NULL) {
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs;
time_t ts = getTimestamp(amst->dt);
if(meterType == AmsTypeKamstrup || meterType == AmsTypeAidon) {
this->meterTimestamp = tz.toUTC(ts);
} else {
meterTimestamp = ts;
}
}
u32 = getUnsignedNumber(AMS_OBIS_POWER_FACTOR, sizeof(AMS_OBIS_POWER_FACTOR), ((char *) (d)));
if(u32 != 0xFFFFFFFF) {
powerFactor = u32 / 100.0;
}
u32 = getUnsignedNumber(AMS_OBIS_POWER_FACTOR_L1, sizeof(AMS_OBIS_POWER_FACTOR_L1), ((char *) (d)));
if(u32 != 0xFFFFFFFF) {
l1PowerFactor = u32 / 100.0;
}
u32 = getUnsignedNumber(AMS_OBIS_POWER_FACTOR_L2, sizeof(AMS_OBIS_POWER_FACTOR_L2), ((char *) (d)));
if(u32 != 0xFFFFFFFF) {
l2PowerFactor = u32 / 100.0;
}
u32 = getUnsignedNumber(AMS_OBIS_POWER_FACTOR_L3, sizeof(AMS_OBIS_POWER_FACTOR_L3), ((char *) (d)));
if(u32 != 0xFFFFFFFF) {
l3PowerFactor = u32 / 100.0;
}
lastUpdateMillis = millis();
}
threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0;
twoPhase = (l1voltage > 0 && l2voltage > 0) || (l2voltage > 0 && l3voltage > 0) || (l3voltage > 0 && l1voltage > 0);
if(threePhase) {
if(l2current == 0 && l1current > 0 && l3current > 0) {
l2current = (((activeImportPower - activeExportPower) * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage;
}
}
}
CosemData* IEC6205675::getCosemDataAt(uint8_t index, const char* ptr) {
CosemData* item = (CosemData*) ptr;
int i = 0;
char* pos = (char*) ptr;
do {
item = (CosemData*) pos;
if(i == index) return item;
switch(item->base.type) {
case CosemTypeArray:
case CosemTypeStructure:
pos += 2;
break;
case CosemTypeOctetString:
case CosemTypeString:
pos += 2 + item->base.length;
break;
case CosemTypeLongSigned:
pos += 5;
break;
case CosemTypeLongUnsigned:
pos += 3;
break;
case CosemTypeDLongUnsigned:
pos += 5;
break;
case CosemTypeNull:
return NULL;
default:
pos += 2;
}
i++;
} while(item->base.type != HDLC_FLAG);
return NULL;
}
CosemData* IEC6205675::findObis(uint8_t* obis, int matchlength, const char* ptr) {
CosemData* item = (CosemData*) ptr;
int ret = 0;
char* pos = (char*) ptr;
do {
item = (CosemData*) pos;
if(ret == 1) return item;
switch(item->base.type) {
case CosemTypeArray:
case CosemTypeStructure:
pos += 2;
break;
case CosemTypeOctetString: {
ret = 1;
uint8_t* found = item->oct.data;
int x = 6 - matchlength;
for(int i = x; i < 6; i++) {
if(found[i] != obis[i-x]) ret = 0;
}
} // Fallthrough
case CosemTypeString: {
pos += 2 + item->base.length;
break;
}
case CosemTypeLongSigned:
pos += 5;
break;
case CosemTypeLongUnsigned:
pos += 3;
break;
case CosemTypeDLongUnsigned:
pos += 5;
break;
case CosemTypeNull:
return NULL;
default:
pos += 2;
}
} while(item->base.type != HDLC_FLAG);
return NULL;
}
uint8_t IEC6205675::getString(uint8_t* obis, int matchlength, const char* ptr, char* target) {
CosemData* item = findObis(obis, matchlength, ptr);
if(item != NULL) {
switch(item->base.type) {
case CosemTypeString:
memcpy(target, item->str.data, item->str.length);
target[item->str.length] = 0;
return item->str.length;
case CosemTypeOctetString:
memcpy(target, item->oct.data, item->oct.length);
target[item->oct.length] = 0;
return item->oct.length;
}
}
return 0;
}
uint32_t IEC6205675::getSignedNumber(uint8_t* obis, int matchlength, const char* ptr) {
CosemData* item = findObis(obis, matchlength, ptr);
if(item != NULL) {
switch(item->base.type) {
case CosemTypeLongUnsigned:
return ntohs(item->lu.data);
case CosemTypeDLongUnsigned:
return ntohl(item->dlu.data);
case CosemTypeLongSigned:
return ntohs(item->lu.data);
}
}
return 0xFFFFFFFF;
}
uint32_t IEC6205675::getUnsignedNumber(uint8_t* obis, int matchlength, const char* ptr) {
CosemData* item = findObis(obis, matchlength, ptr);
if(item != NULL) {
switch(item->base.type) {
case CosemTypeLongUnsigned:
return ntohs(item->lu.data);
case CosemTypeDLongUnsigned:
return ntohl(item->dlu.data);
}
}
return 0xFFFFFFFF;
}
time_t IEC6205675::getTimestamp(uint8_t* obis, int matchlength, const char* ptr) {
CosemData* item = findObis(obis, matchlength, ptr);
if(item != NULL) {
switch(item->base.type) {
case CosemTypeOctetString: {
if(item->oct.length == 0x0C) {
AmsOctetTimestamp* ts = (AmsOctetTimestamp*) item;
return getTimestamp(ts->dt);
}
}
}
}
return 0;
}
time_t IEC6205675::getTimestamp(CosemDateTime timestamp) {
tmElements_t tm;
uint16_t year = ntohs(timestamp.year);
if(year < 1970) return 0;
tm.Year = year - 1970;
tm.Month = timestamp.month;
tm.Day = timestamp.dayOfMonth;
tm.Hour = timestamp.hour;
tm.Minute = timestamp.minute;
tm.Second = timestamp.second;
//Serial.printf("\nY: %d, M: %d, D: %d, h: %d, m: %d, s: %d, deviation: 0x%2X, status: 0x%1X\n", tm.Year, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second, timestamp.deviation, timestamp.status);
time_t time = makeTime(tm);
int16_t deviation = ntohs(timestamp.deviation);
if(deviation >= -720 && deviation <= 720) {
time -= deviation * 60;
}
return time;
}

65
src/IEC6205675.h Normal file
View File

@@ -0,0 +1,65 @@
#ifndef _IEC62056_7_5_H
#define _IEC62056_7_5_H
#include "AmsData.h"
#include "ams/hdlc.h"
struct AmsOctetTimestamp {
uint8_t type;
CosemDateTime dt;
} __attribute__((packed));
class IEC6205675 : public AmsData {
public:
IEC6205675(const char* payload, uint8_t useMeterType, CosemDateTime packageTimestamp);
private:
CosemData* getCosemDataAt(uint8_t index, const char* ptr);
CosemData* findObis(uint8_t* obis, int matchlength, const char* ptr);
uint8_t getString(uint8_t* obis, int matchlength, const char* ptr, char* target);
uint32_t getSignedNumber(uint8_t* obis, int matchlength, const char* ptr);
uint32_t getUnsignedNumber(uint8_t* obis, int matchlength, const char* ptr);
time_t getTimestamp(uint8_t* obis, int matchlength, const char* ptr);
time_t getTimestamp(CosemDateTime timestamp);
uint8_t AMS_OBIS_VERSION[6] = { 1, 1, 0, 2, 129, 255 };
uint8_t AMS_OBIS_METER_MODEL[4] = { 96, 1, 1, 255 };
uint8_t AMS_OBIS_METER_MODEL_2[4] = { 96, 1, 7, 255 };
uint8_t AMS_OBIS_METER_ID[4] = { 96, 1, 0, 255 };
uint8_t AMS_OBIS_METER_ID_2[4] = { 0, 0, 5, 255 };
uint8_t AMS_OBIS_METER_TIMESTAMP[4] = { 1, 0, 0, 255 };
uint8_t AMS_OBIS_ACTIVE_IMPORT[4] = { 1, 7, 0, 255 };
uint8_t AMS_OBIS_ACTIVE_IMPORT_L1[4] = { 21, 7, 0, 255 };
uint8_t AMS_OBIS_ACTIVE_IMPORT_L2[4] = { 41, 7, 0, 255 };
uint8_t AMS_OBIS_ACTIVE_IMPORT_L3[4] = { 61, 7, 0, 255 };
uint8_t AMS_OBIS_ACTIVE_EXPORT[4] = { 2, 7, 0, 255 };
uint8_t AMS_OBIS_ACTIVE_EXPORT_L1[4] = { 22, 7, 0, 255 };
uint8_t AMS_OBIS_ACTIVE_EXPORT_L2[4] = { 42, 7, 0, 255 };
uint8_t AMS_OBIS_ACTIVE_EXPORT_L3[4] = { 62, 7, 0, 255 };
uint8_t AMS_OBIS_REACTIVE_IMPORT[4] = { 3, 7, 0, 255 };
uint8_t AMS_OBIS_REACTIVE_IMPORT_L1[4] = { 23, 7, 0, 255 };
uint8_t AMS_OBIS_REACTIVE_IMPORT_L2[4] = { 43, 7, 0, 255 };
uint8_t AMS_OBIS_REACTIVE_IMPORT_L3[4] = { 63, 7, 0, 255 };
uint8_t AMS_OBIS_REACTIVE_EXPORT[4] = { 4, 7, 0, 255 };
uint8_t AMS_OBIS_REACTIVE_EXPORT_L1[4] = { 24, 7, 0, 255 };
uint8_t AMS_OBIS_REACTIVE_EXPORT_L2[4] = { 44, 7, 0, 255 };
uint8_t AMS_OBIS_REACTIVE_EXPORT_L3[4] = { 64, 7, 0, 255 };
uint8_t AMS_OBIS_CURRENT[4] = { 11, 7, 0, 255 };
uint8_t AMS_OBIS_CURRENT_L1[4] = { 31, 7, 0, 255 };
uint8_t AMS_OBIS_CURRENT_L2[4] = { 51, 7, 0, 255 };
uint8_t AMS_OBIS_CURRENT_L3[4] = { 71, 7, 0, 255 };
uint8_t AMS_OBIS_VOLTAGE[4] = { 12, 7, 0, 255 };
uint8_t AMS_OBIS_VOLTAGE_L1[4] = { 32, 7, 0, 255 };
uint8_t AMS_OBIS_VOLTAGE_L2[4] = { 52, 7, 0, 255 };
uint8_t AMS_OBIS_VOLTAGE_L3[4] = { 72, 7, 0, 255 };
uint8_t AMS_OBIS_ACTIVE_IMPORT_COUNT[4] = { 1, 8, 0, 255 };
uint8_t AMS_OBIS_ACTIVE_EXPORT_COUNT[4] = { 2, 8, 0, 255 };
uint8_t AMS_OBIS_REACTIVE_IMPORT_COUNT[4] = { 3, 8, 0, 255 };
uint8_t AMS_OBIS_REACTIVE_EXPORT_COUNT[4] = { 4, 8, 0, 255 };
uint8_t AMS_OBIS_POWER_FACTOR[4] = { 13, 7, 0, 255 };
uint8_t AMS_OBIS_POWER_FACTOR_L1[4] = { 33, 7, 0, 255 };
uint8_t AMS_OBIS_POWER_FACTOR_L2[4] = { 53, 7, 0, 255 };
uint8_t AMS_OBIS_POWER_FACTOR_L3[4] = { 73, 7, 0, 255 };
};
#endif

12
src/ams/crc.cpp Normal file
View File

@@ -0,0 +1,12 @@
#include "crc.h"
uint16_t crc16_x25(const uint8_t* p, int len)
{
uint16_t crc = UINT16_MAX;
while(len--)
for (uint16_t i = 0, d = 0xff & *p++; i < 8; i++, d >>= 1)
crc = ((crc & 1) ^ (d & 1)) ? (crc >> 1) ^ 0x8408 : (crc >> 1);
return (~crc << 8) | (~crc >> 8 & 0xff);
}

9
src/ams/crc.h Normal file
View File

@@ -0,0 +1,9 @@
#ifndef _CRC_H
#define _CRC_H
#include "Arduino.h"
#include <stdint.h>
uint16_t crc16_x25(const uint8_t* p, int len);
#endif

232
src/ams/hdlc.cpp Normal file
View File

@@ -0,0 +1,232 @@
#include "Arduino.h"
#include "hdlc.h"
#include "crc.h"
#include "lwip/def.h"
#if defined(ESP8266)
#include "bearssl/bearssl.h"
#elif defined(ESP32)
#include "mbedtls/gcm.h"
#endif
void mbus_hexdump(const uint8_t* buf, int len) {
printf("\nDUMP (%db) [ ", len);
for(const uint8_t* p = buf; p-buf < len; ++p)
printf("%02X ", *p);
printf("]\n");
}
int HDLC_validate(const uint8_t* d, int length, HDLCConfig* config, CosemDateTime* timestamp) {
if(length < 10)
return HDLC_FRAME_INCOMPLETE;
int len;
int headersize = 3;
int footersize = 1;
HDLCHeader* h = (HDLCHeader*) d;
uint8_t* ptr = (uint8_t*) &h[1];
// Frame format type 3
if(h->flag == HDLC_FLAG && (h->format & 0xF0) == 0xA0) {
// Length field (11 lsb of format)
len = (ntohs(h->format) & 0x7FF) + 2;
if(len > length)
return HDLC_FRAME_INCOMPLETE;
HDLCFooter* f = (HDLCFooter*) (d + len - sizeof *f);
footersize = sizeof *f;
// First and last byte should be MBUS_HAN_TAG
if(h->flag != HDLC_FLAG || f->flag != HDLC_FLAG)
return HDLC_BOUNDRY_FLAG_MISSING;
// Verify FCS
if(ntohs(f->fcs) != crc16_x25(d + 1, len - sizeof *f - 1))
return HDLC_FCS_ERROR;
// Skip destination address, LSB marks last byte
while(((*ptr) & 0x01) == 0x00) {
ptr++;
headersize++;
}
headersize++;
ptr++;
// Skip source address, LSB marks last byte
while(((*ptr) & 0x01) == 0x00) {
ptr++;
headersize++;
}
headersize++;
ptr++;
HDLC3CtrlHcs* t3 = (HDLC3CtrlHcs*) (ptr);
headersize += 3;
// Verify HCS
if(ntohs(t3->hcs) != crc16_x25(d + 1, ptr-d))
return HDLC_HCS_ERROR;
ptr += sizeof *t3;
} else if(h->flag == MBUS_START) {
// TODO: Check that the two next bytes are identical
// Ignore: Control field + Address + Flag
ptr += 3;
headersize += 3;
footersize++;
}
// Extract LLC
HDLCLLC* llc = (HDLCLLC*) ptr;
ptr += sizeof *llc;
headersize += 3;
if(((*ptr) & 0xFF) == 0x0F) {
// Unencrypted APDU
int i = 0;
HDLCADPU* adpu = (HDLCADPU*) (ptr);
ptr += sizeof *adpu;
// ADPU timestamp
CosemData* dateTime = (CosemData*) ptr;
if(dateTime->base.type == CosemTypeOctetString) {
if(dateTime->base.length == 0x0C) {
memcpy(timestamp, ptr+1, dateTime->base.length);
}
ptr += 2 + dateTime->base.length;
} else if(dateTime->base.type == CosemTypeNull) {
timestamp = 0;
ptr++;
} else if(dateTime->base.type == CosemTypeDateTime) {
memcpy(timestamp, ptr, dateTime->base.length);
} else if(dateTime->base.type == 0x0C) { // Kamstrup bug...
memcpy(timestamp, ptr, 0x0C);
ptr += 13;
} else {
return -99;
}
return ptr-d;
} else if(((*ptr) & 0xFF) == 0xDB) {
if(length < headersize + 18)
return HDLC_FRAME_INCOMPLETE;
ptr++;
// Encrypted APDU
// http://www.weigu.lu/tutorials/sensors2bus/04_encryption/index.html
if(config == NULL)
return HDLC_ENCRYPTION_CONFIG_MISSING;
uint8_t systemTitleLength = *ptr;
ptr++;
memcpy(config->system_title, ptr, systemTitleLength);
memcpy(config->initialization_vector, config->system_title, systemTitleLength);
headersize += 2 + systemTitleLength;
ptr += systemTitleLength;
if(((*ptr) & 0xFF) == 0x81) {
ptr++;
len = *ptr;
// 1-byte payload length
ptr++;
headersize += 2;
} else if(((*ptr) & 0xFF) == 0x82) {
HDLCHeader* h = (HDLCHeader*) ptr;
// Length field
len = (ntohs(h->format) & 0xFFFF);
ptr += 3;
headersize += 3;
}
//len = ceil(len/16.0) * 16; // Technically GCM is 128bit blocks. This works for Austrian meters, but not Danish...
if(len + headersize + footersize > length)
return HDLC_FRAME_INCOMPLETE;
//Serial.printf("\nL: %d : %d, %d : %d\n", length, len, headersize, footersize);
// TODO: FCS
memcpy(config->additional_authenticated_data, ptr, 1);
// Security tag
uint8_t sec = *ptr;
ptr++;
headersize++;
// Frame counter
memcpy(config->initialization_vector + 8, ptr, 4);
ptr += 4;
headersize += 4;
// Authentication enabled
uint8_t authkeylen = 0, aadlen = 0;
if((sec & 0x10) == 0x10) {
authkeylen = 12;
aadlen = 17;
footersize += authkeylen;
memcpy(config->additional_authenticated_data + 1, config->authentication_key, 16);
memcpy(config->authentication_tag, ptr + len - footersize - 2, authkeylen);
}
#if defined(ESP8266)
br_gcm_context gcmCtx;
br_aes_ct_ctr_keys bc;
br_aes_ct_ctr_init(&bc, config->encryption_key, 16);
br_gcm_init(&gcmCtx, &bc.vtable, br_ghash_ctmul32);
br_gcm_reset(&gcmCtx, config->initialization_vector, sizeof(config->initialization_vector));
if(authkeylen > 0) {
br_gcm_aad_inject(&gcmCtx, config->additional_authenticated_data, aadlen);
}
br_gcm_flip(&gcmCtx);
br_gcm_run(&gcmCtx, 0, (void*) (ptr), len - authkeylen - 5); // 5 == security tag and frame counter
if(authkeylen > 0 && br_gcm_check_tag_trunc(&gcmCtx, config->authentication_tag, authkeylen) != 1) {
return HDLC_ENCRYPTION_AUTH_FAILED;
}
#elif defined(ESP32)
uint8_t cipher_text[len - authkeylen - 5];
memcpy(cipher_text, ptr, len - authkeylen - 5);
mbedtls_gcm_context m_ctx;
mbedtls_gcm_init(&m_ctx);
int success = mbedtls_gcm_setkey(&m_ctx, MBEDTLS_CIPHER_ID_AES, config->encryption_key, 128);
if (0 != success) {
return HDLC_ENCRYPTION_KEY_FAILED;
}
success = mbedtls_gcm_auth_decrypt(&m_ctx, sizeof(cipher_text), config->initialization_vector, sizeof(config->initialization_vector),
config->additional_authenticated_data, aadlen, config->authentication_tag, authkeylen,
cipher_text, (unsigned char*)(ptr));
if (authkeylen > 0 && success == MBEDTLS_ERR_GCM_AUTH_FAILED) {
return HDLC_ENCRYPTION_AUTH_FAILED;
} else if(success == MBEDTLS_ERR_GCM_BAD_INPUT) {
return HDLC_ENCRYPTION_DECRYPT_FAILED;
}
mbedtls_gcm_free(&m_ctx);
#endif
ptr += 5; // TODO: Come to this number in a proper way...
// ADPU timestamp
CosemData* dateTime = (CosemData*) ptr;
if(dateTime->base.type == CosemTypeOctetString) {
if(dateTime->base.length == 0x0C) {
memcpy(timestamp, ptr+1, dateTime->base.length);
}
ptr += 2 + dateTime->base.length;
} else if(dateTime->base.type == CosemTypeNull) {
timestamp = 0;
ptr++;
} else if(dateTime->base.type == CosemTypeDateTime) {
memcpy(timestamp, ptr, dateTime->base.length);
} else if(dateTime->base.type == 0x0C) { // Kamstrup bug...
memcpy(timestamp, ptr, 0x0C);
ptr += 13;
} else {
return -99;
}
return ptr-d;
}
// Unknown payload
return HDLC_UNKNOWN_DATA;
}

129
src/ams/hdlc.h Normal file
View File

@@ -0,0 +1,129 @@
#ifndef _HDLC_H
#define _HDLC_H
#include "Arduino.h"
#include <stdint.h>
#define HDLC_FLAG 0x7E
#define HDLC_BOUNDRY_FLAG_MISSING -1
#define HDLC_FCS_ERROR -2
#define HDLC_HCS_ERROR -3
#define HDLC_FRAME_INCOMPLETE -4
#define HDLC_UNKNOWN_DATA -9
#define HDLC_ENCRYPTION_CONFIG_MISSING -90
#define HDLC_ENCRYPTION_AUTH_FAILED -91
#define HDLC_ENCRYPTION_KEY_FAILED -92
#define HDLC_ENCRYPTION_DECRYPT_FAILED -93
#define MBUS_START 0x68
#define MBUS_END 0x16
struct HDLCConfig {
uint8_t encryption_key[32];
uint8_t authentication_key[32];
uint8_t system_title[8];
uint8_t initialization_vector[12];
uint8_t additional_authenticated_data[17];
uint8_t authentication_tag[12];
};
typedef struct HDLCHeader {
uint8_t flag;
uint16_t format;
} __attribute__((packed)) HDLCHeader;
typedef struct HDLCFooter {
uint16_t fcs;
uint8_t flag;
} __attribute__((packed)) HDLCFooter;
typedef struct HDLC3CtrlHcs {
uint8_t control;
uint16_t hcs;
} __attribute__((packed)) HDLC3CtrlHcs;
typedef struct HDLCLLC {
uint8_t dst;
uint8_t src;
uint8_t control;
} __attribute__((packed)) HDLCLLC;
typedef struct HDLCADPU {
uint8_t flag;
uint32_t id;
} __attribute__((packed)) HDLCADPU;
typedef struct MbusFooter {
uint8_t fcs;
uint8_t flag;
} __attribute__((packed)) MbusFooter;
// Blue book, Table 2
enum CosemType {
CosemTypeNull = 0x00,
CosemTypeArray = 0x01,
CosemTypeStructure = 0x02,
CosemTypeOctetString = 0x09,
CosemTypeString = 0x0A,
CosemTypeDLongUnsigned = 0x06,
CosemTypeLongSigned = 0x10,
CosemTypeLongUnsigned = 0x12,
CosemTypeDateTime = 0x19
};
struct CosemBasic {
uint8_t type;
uint8_t length;
} __attribute__((packed));
struct CosemString {
uint8_t type;
uint8_t length;
uint8_t data[];
} __attribute__((packed));
struct CosemLongUnsigned {
uint8_t type;
uint16_t data;
} __attribute__((packed));
struct CosemDLongUnsigned {
uint8_t type;
uint32_t data;
} __attribute__((packed));
struct CosemLongSigned {
uint8_t type;
int16_t data;
} __attribute__((packed));
struct CosemDateTime {
uint8_t type;
uint16_t year;
uint8_t month;
uint8_t dayOfMonth;
uint8_t dayOfWeek;
uint8_t hour;
uint8_t minute;
uint8_t second;
uint8_t hundredths;
int16_t deviation;
uint8_t status;
} __attribute__((packed));
typedef union {
struct CosemBasic base;
struct CosemString str;
struct CosemString oct;
struct CosemLongUnsigned lu;
struct CosemDLongUnsigned dlu;
struct CosemLongSigned ls;
struct CosemDateTime dt;
} CosemData;
void mbus_hexdump(const uint8_t* buf, int len);
int HDLC_validate(const uint8_t* d, int len, HDLCConfig* config, CosemDateTime* timestamp);
#endif

View File

@@ -0,0 +1,60 @@
#include "DnbCurrParser.h"
#include "HardwareSerial.h"
float DnbCurrParser::getValue() {
return value;
}
int DnbCurrParser::available() {
return 0;
}
int DnbCurrParser::read() {
return 0;
}
int DnbCurrParser::peek() {
return 0;
}
void DnbCurrParser::flush() {
}
size_t DnbCurrParser::write(const uint8_t *buffer, size_t size) {
for(int i = 0; i < size; i++) {
write(buffer[i]);
}
return size;
}
size_t DnbCurrParser::write(uint8_t byte) {
if(pos == 0) {
if(byte == '<') {
buf[pos++] = byte;
}
} else if(byte == '>') {
buf[pos++] = byte;
if(strncmp(buf, "<Obs", 4) == 0) {
for(int i = 0; i < pos; i++) {
if(strncmp(buf+i, "OBS_VALUE=\"", 11) == 0) {
pos = i + 11;
break;
}
}
for(int i = 0; i < 16; i++) {
uint8_t b = buf[pos+i];
if(b == '"') {
buf[pos+i] = '\0';
break;
}
}
value = String(buf+pos).toFloat();
}
pos = 0;
} else {
buf[pos++] = byte;
}
return 1;
}

View File

@@ -0,0 +1,25 @@
#ifndef _DNBCURRPARSER_H
#define _DNBCURRPARSER_H
#include "Stream.h"
class DnbCurrParser: public Stream {
public:
float getValue();
int available();
int read();
int peek();
void flush();
size_t write(const uint8_t *buffer, size_t size);
size_t write(uint8_t);
private:
float value = 1.0;
char buf[64];
uint8_t pos = 0;
uint8_t mode = 0;
};
#endif

View File

@@ -0,0 +1,102 @@
#include "EntsoeA44Parser.h"
#include "HardwareSerial.h"
EntsoeA44Parser::EntsoeA44Parser() {
for(int i = 0; i < 24; i++) points[i] = 0.0;
}
char* EntsoeA44Parser::getCurrency() {
return currency;
}
char* EntsoeA44Parser::getMeasurementUnit() {
return measurementUnit;
}
float EntsoeA44Parser::getPoint(uint8_t position) {
return points[position];
}
int EntsoeA44Parser::available() {
return 0;
}
int EntsoeA44Parser::read() {
return 0;
}
int EntsoeA44Parser::peek() {
return 0;
}
void EntsoeA44Parser::flush() {
}
size_t EntsoeA44Parser::write(const uint8_t *buffer, size_t size) {
for(int i = 0; i < size; i++) {
write(buffer[i]);
}
return size;
}
size_t EntsoeA44Parser::write(uint8_t byte) {
if(docPos == DOCPOS_CURRENCY) {
buf[pos++] = byte;
if(pos == 3) {
buf[pos++] = '\0';
memcpy(currency, buf, pos);
docPos = DOCPOS_SEEK;
pos = 0;
}
} else if(docPos == DOCPOS_MEASUREMENTUNIT) {
buf[pos++] = byte;
if(pos == 3) {
buf[pos++] = '\0';
memcpy(measurementUnit, buf, pos);
docPos = DOCPOS_SEEK;
pos = 0;
}
} else if(docPos == DOCPOS_POSITION) {
if(byte == '<') {
buf[pos] = '\0';
pointNum = String(buf).toInt() - 1;
docPos = DOCPOS_SEEK;
pos = 0;
} else {
buf[pos++] = byte;
}
} else if(docPos == DOCPOS_AMOUNT) {
if(byte == '<') {
buf[pos] = '\0';
points[pointNum] = String(buf).toFloat();
docPos = DOCPOS_SEEK;
pos = 0;
} else {
buf[pos++] = byte;
}
} else {
if(pos == 0) {
if(byte == '<') {
buf[pos++] = byte;
}
} else if(byte == '>') {
buf[pos++] = byte;
buf[pos] = '\0';
if(strcmp(buf, "<currency_Unit.name>") == 0) {
docPos = DOCPOS_CURRENCY;
} else if(strcmp(buf, "<price_Measure_Unit.name>") == 0) {
docPos = DOCPOS_MEASUREMENTUNIT;
} else if(strcmp(buf, "<position>") == 0) {
docPos = DOCPOS_POSITION;
pointNum = 0xFF;
} else if(strcmp(buf, "<price.amount>") == 0) {
docPos = DOCPOS_AMOUNT;
}
pos = 0;
} else {
buf[pos++] = byte;
}
}
return 1;
}

View File

@@ -0,0 +1,38 @@
#ifndef _ENTSOEA44PARSER_H
#define _ENTSOEA44PARSER_H
#include "Stream.h"
#define DOCPOS_SEEK 0
#define DOCPOS_CURRENCY 1
#define DOCPOS_MEASUREMENTUNIT 2
#define DOCPOS_POSITION 3
#define DOCPOS_AMOUNT 4
class EntsoeA44Parser: public Stream {
public:
EntsoeA44Parser();
char* getCurrency();
char* getMeasurementUnit();
float getPoint(uint8_t position);
int available();
int read();
int peek();
void flush();
size_t write(const uint8_t *buffer, size_t size);
size_t write(uint8_t);
private:
char currency[4];
char measurementUnit[4];
float points[24];
char buf[64];
uint8_t pos = 0;
uint8_t docPos = 0;
uint8_t pointNum = 0;
};
#endif

284
src/entsoe/EntsoeApi.cpp Normal file
View File

@@ -0,0 +1,284 @@
#include "EntsoeApi.h"
#include <EEPROM.h>
#include "Uptime.h"
#include "Time.h"
#include "DnbCurrParser.h"
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
#include <HTTPClient.h>
#else
#warning "Unsupported board type"
#endif
EntsoeApi::EntsoeApi(RemoteDebug* Debug) {
debugger = Debug;
// Entso-E uses CET/CEST
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
tz = new Timezone(CEST, CET);
}
void EntsoeApi::setup(EntsoeConfig& config) {
if(this->config == NULL) {
this->config = new EntsoeConfig();
}
memcpy(this->config, &config, sizeof(config));
}
char* EntsoeApi::getToken() {
return this->config->token;
}
char* EntsoeApi::getCurrency() {
return this->config->currency;
}
float EntsoeApi::getValueForHour(uint8_t hour) {
time_t cur = time(nullptr);
return getValueForHour(cur, hour);
}
float EntsoeApi::getValueForHour(time_t cur, uint8_t hour) {
tmElements_t tm;
if(tz != NULL)
cur = tz->toLocal(cur);
breakTime(cur, tm);
int pos = tm.Hour + hour;
if(pos >= 48)
return ENTSOE_NO_VALUE;
double value = ENTSOE_NO_VALUE;
double multiplier = config->multiplier / 1000.0;
if(pos > 23) {
if(tomorrow == NULL)
return ENTSOE_NO_VALUE;
value = tomorrow->getPoint(pos-24);
if(strcmp(tomorrow->getMeasurementUnit(), "MWH") == 0) {
multiplier *= 0.001;
} else {
return ENTSOE_NO_VALUE;
}
multiplier *= getCurrencyMultiplier(tomorrow->getCurrency(), config->currency);
} else {
if(today == NULL)
return ENTSOE_NO_VALUE;
value = today->getPoint(pos);
if(strcmp(today->getMeasurementUnit(), "MWH") == 0) {
multiplier *= 0.001;
} else {
return ENTSOE_NO_VALUE;
}
multiplier *= getCurrencyMultiplier(today->getCurrency(), config->currency);
}
return value * multiplier;
}
bool EntsoeApi::loop() {
if(strlen(getToken()) == 0)
return false;
bool ret = false;
uint64_t now = millis64();
if(now < 10000) return false; // Grace period
if(midnightMillis == 0) {
time_t t = time(nullptr);
if(t <= 0) return false; // NTP not ready
time_t epoch = tz->toLocal(t);
tmElements_t tm;
breakTime(epoch, tm);
if(tm.Year > 50) { // Make sure we are in 2021 or later (years after 1970)
uint64_t curDeviceMillis = millis64();
uint32_t curDayMillis = (((((tm.Hour * 60) + tm.Minute) * 60) + tm.Second) * 1000);
midnightMillis = curDeviceMillis + (SECS_PER_DAY * 1000) - curDayMillis;
printI("Setting midnight millis " + String((uint32_t) midnightMillis));
}
} else if(now > midnightMillis) {
printI("Rotating price objects");
delete today;
today = tomorrow;
tomorrow = NULL;
midnightMillis = 0; // Force new midnight millis calculation
} else {
if(today == NULL && (lastTodayFetch == 0 || now - lastTodayFetch > 60000)) {
lastTodayFetch = now;
time_t e1 = time(nullptr) - (SECS_PER_DAY * 1);
time_t e2 = e1 + SECS_PER_DAY;
tmElements_t d1, d2;
breakTime(e1, d1);
breakTime(e2, d2);
char url[256];
snprintf(url, sizeof(url), "%s?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s",
"https://transparency.entsoe.eu/api", getToken(),
d1.Year+1970, d1.Month, d1.Day, 23, 00,
d2.Year+1970, d2.Month, d2.Day, 23, 00,
config->area, config->area);
printI("Fetching prices for today");
printD(url);
EntsoeA44Parser* a44 = new EntsoeA44Parser();
if(retrieve(url, a44)) {
today = a44;
ret = true;
} else {
delete a44;
today = NULL;
}
}
if(tomorrow == NULL
&& midnightMillis - now < 39600000
&& (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 60000)
) {
lastTomorrowFetch = now;
time_t e1 = time(nullptr);
time_t e2 = e1 + SECS_PER_DAY;
tmElements_t d1, d2;
breakTime(e1, d1);
breakTime(e2, d2);
char url[256];
snprintf(url, sizeof(url), "%s?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s",
"https://transparency.entsoe.eu/api", getToken(),
d1.Year+1970, d1.Month, d1.Day, 23, 00,
d2.Year+1970, d2.Month, d2.Day, 23, 00,
config->area, config->area);
printI("Fetching prices for tomorrow");
printD(url);
EntsoeA44Parser* a44 = new EntsoeA44Parser();
if(retrieve(url, a44)) {
tomorrow = a44;
ret = true;
} else {
delete a44;
tomorrow = NULL;
}
}
}
return ret;
}
bool EntsoeApi::retrieve(const char* url, Stream* doc) {
WiFiClientSecure client;
#if defined(ESP8266)
// https://arduino-esp8266.readthedocs.io/en/latest/esp8266wifi/bearssl-client-secure-class.html#mfln-or-maximum-fragment-length-negotiation-saving-ram
/* Rumor has it that a client cannot request a lower max_fragment_length, so I guess thats why the following does not work.
And there is currently not enough heap space to go around in this project to do a full HTTPS request on ESP8266
int bufSize = 512;
while(!client.probeMaxFragmentLength("transparency.entsoe.eu", 443, bufSize) && bufSize <= 4096) {
bufSize += 512;
}
if(client.probeMaxFragmentLength("transparency.entsoe.eu", 443, bufSize)) {
printD("Negotiated MFLN size");
printD(String(bufSize));
client.setBufferSizes(bufSize, bufSize);
}
*/
#endif
client.setInsecure();
HTTPClient https;
https.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
if(https.begin(client, url)) {
printD("Connection established");
/*
#if defined(ESP8266)
if(!client.getMFLNStatus()) {
printE("Negotiated MFLN was not respected");
https.end();
client.stop();
return false;
}
#endif
*/
int status = https.GET();
if(status == HTTP_CODE_OK) {
printD("Receiving data");
https.writeToStream(doc);
https.end();
return true;
} else {
printE("Communication error: ");
printE(https.errorToString(status));
printD(https.getString());
#if defined(ESP8266)
char buf[64];
client.getLastSSLError(buf,64);
printE(buf);
#endif
https.end();
return false;
}
} else {
#if defined(ESP8266)
char buf[64];
client.getLastSSLError(buf,64);
printE(buf);
#endif
return false;
}
client.stop();
}
float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to) {
if(strcmp(from, to) == 0)
return 1.00;
uint64_t now = millis64();
if(lastCurrencyFetch == 0 || now - lastCurrencyFetch > (SECS_PER_HOUR * 1000)) {
char url[256];
snprintf(url, sizeof(url), "https://data.norges-bank.no/api/data/EXR/M.%s.%s.SP?lastNObservations=1",
from,
to
);
DnbCurrParser p;
if(retrieve(url, &p)) {
currencyMultiplier = p.getValue();
}
lastCurrencyFetch = now;
}
return currencyMultiplier;
}
void EntsoeApi::printD(String fmt, ...) {
va_list args;
va_start(args, fmt);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args);
va_end(args);
}
void EntsoeApi::printI(String fmt, ...) {
va_list args;
va_start(args, fmt);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args);
va_end(args);
}
void EntsoeApi::printW(String fmt, ...) {
va_list args;
va_start(args, fmt);
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args);
va_end(args);
}
void EntsoeApi::printE(String fmt, ...) {
va_list args;
va_start(args, fmt);
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args);
va_end(args);
}

48
src/entsoe/EntsoeApi.h Normal file
View File

@@ -0,0 +1,48 @@
#ifndef _ENTSOEAPI_H
#define _ENTSOEAPI_H
#include "time.h"
#include "Timezone.h"
#include "RemoteDebug.h"
#include "EntsoeA44Parser.h"
#include "AmsConfiguration.h"
#define ENTSOE_NO_VALUE -127
#define ENTSOE_DEFAULT_MULTIPLIER 1.00
#define SSL_BUF_SIZE 512
class EntsoeApi {
public:
EntsoeApi(RemoteDebug*);
void setup(EntsoeConfig&);
bool loop();
char* getToken();
char* getCurrency();
float getValueForHour(uint8_t);
float getValueForHour(time_t, uint8_t);
private:
RemoteDebug* debugger;
EntsoeConfig* config = NULL;
uint64_t midnightMillis = 0;
uint64_t lastTodayFetch = 0;
uint64_t lastTomorrowFetch = 0;
uint64_t lastCurrencyFetch = 0;
EntsoeA44Parser* today = NULL;
EntsoeA44Parser* tomorrow = NULL;
Timezone* tz = NULL;
float currencyMultiplier = ENTSOE_DEFAULT_MULTIPLIER;
bool retrieve(const char* url, Stream* doc);
float getCurrencyMultiplier(const char* from, const char* to);
void printD(String fmt, ...);
void printI(String fmt, ...);
void printW(String fmt, ...);
void printE(String fmt, ...);
};
#endif

23
src/hexutils.cpp Normal file
View File

@@ -0,0 +1,23 @@
#include "hexutils.h";
String toHex(uint8_t* in) {
return toHex(in, sizeof(in)*2);
}
String toHex(uint8_t* in, uint8_t size) {
String hex;
for(int i = 0; i < size; i++) {
if(in[i] < 0x10) {
hex += '0';
}
hex += String(in[i], HEX);
}
hex.toUpperCase();
return hex;
}
void fromHex(uint8_t *out, String in, uint8_t size) {
for(int i = 0; i < size*2; i += 2) {
out[i/2] = strtol(in.substring(i, i+2).c_str(), 0, 16);
}
}

11
src/hexutils.h Normal file
View File

@@ -0,0 +1,11 @@
#ifndef _HEXUTILS_H
#define _HEXUTILS_H
#include <stdint.h>
#include "Arduino.h"
String toHex(uint8_t* in);
String toHex(uint8_t* in, uint8_t size);
void fromHex(uint8_t *out, String in, uint8_t size);
#endif

26
src/mqtt/AmsMqttHandler.h Normal file
View File

@@ -0,0 +1,26 @@
#ifndef _AMSMQTTHANDLER_H
#define _AMSMQTTHANDLER_H
#include "Arduino.h"
#include <MQTT.h>
#include "AmsData.h"
#include "AmsConfiguration.h"
#include "HwTools.h"
#include "entsoe/EntsoeApi.h"
class AmsMqttHandler {
public:
AmsMqttHandler(MQTTClient* mqtt) {
this->mqtt = mqtt;
};
virtual bool publish(AmsData* data, AmsData* previousState);
virtual bool publishTemperatures(AmsConfiguration*, HwTools*);
virtual bool publishPrices(EntsoeApi* eapi);
virtual bool publishSystem(HwTools*);
protected:
MQTTClient* mqtt;
};
#endif

View File

@@ -0,0 +1,81 @@
#include "DomoticzMqttHandler.h"
#include "web/root/domoticz_json.h"
bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState) {
bool ret = false;
if (config.elidx > 0) {
if(data->getActiveImportCounter() > 1.0) {
energy = data->getActiveImportCounter();
}
if(energy > 0.0) {
char val[16];
snprintf(val, 16, "%.1f;%.1f", (data->getActiveImportPower()/1.0), energy*1000.0);
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.elidx,
val
);
ret = mqtt->publish("domoticz/in", json);
}
}
if(data->getListType() == 1)
return ret;
if (config.vl1idx > 0){
char val[16];
snprintf(val, 16, "%.2f", data->getL1Voltage());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.vl1idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
if (config.vl2idx > 0){
char val[16];
snprintf(val, 16, "%.2f", data->getL2Voltage());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.vl2idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
if (config.vl3idx > 0){
char val[16];
snprintf(val, 16, "%.2f", data->getL3Voltage());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.vl3idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
if (config.cl1idx > 0){
char val[16];
snprintf(val, 16, "%.1f;%.1f;%.1f", data->getL1Current(), data->getL2Current(), data->getL3Current());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.cl1idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
return ret;
}
bool DomoticzMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
return false;
}
bool DomoticzMqttHandler::publishPrices(EntsoeApi* eapi) {
return false;
}
bool DomoticzMqttHandler::publishSystem(HwTools* hw) {
return false;
}

View File

@@ -0,0 +1,21 @@
#ifndef _DOMOTICZMQTTHANDLER_H
#define _DOMOTICZMQTTHANDLER_H
#include "AmsMqttHandler.h"
#include "AmsConfiguration.h"
class DomoticzMqttHandler : public AmsMqttHandler {
public:
DomoticzMqttHandler(MQTTClient* mqtt, DomoticzConfig config) : AmsMqttHandler(mqtt) {
this->config = config;
};
bool publish(AmsData* data, AmsData* previousState);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*);
private:
DomoticzConfig config;
int energy = 0.0;
};
#endif

View File

@@ -0,0 +1,234 @@
#include "JsonMqttHandler.h"
#include "hexutils.h"
#include "Uptime.h"
#include "web/root/json1_json.h"
#include "web/root/json2_json.h"
#include "web/root/json3_json.h"
#include "web/root/jsonsys_json.h"
#include "web/root/jsonprices_json.h"
bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(data->getListType() == 1) {
char json[192];
snprintf_P(json, sizeof(json), JSON1_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature(),
data->getActiveImportPower()
);
return mqtt->publish(topic, json);
} else if(data->getListType() == 2) {
char json[384];
snprintf_P(json, sizeof(json), JSON2_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature(),
data->getListId().c_str(),
data->getMeterId().c_str(),
data->getMeterModel().c_str(),
data->getActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage()
);
return mqtt->publish(topic, json);
} else if(data->getListType() == 3) {
char json[512];
snprintf_P(json, sizeof(json), JSON3_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
data->getPackageTimestamp(),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature(),
data->getListId().c_str(),
data->getMeterId().c_str(),
data->getMeterModel().c_str(),
data->getActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage(),
data->getActiveImportCounter(),
data->getActiveExportCounter(),
data->getReactiveImportCounter(),
data->getReactiveExportCounter(),
data->getMeterTimestamp()
);
return mqtt->publish(topic, json);
}
return false;
}
bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
int count = hw->getTempSensorCount();
if(count == 0)
return false;
int size = 32 + (count * 26);
char buf[size];
snprintf(buf, 24, "{\"temperatures\":{");
for(int i = 0; i < count; i++) {
TempSensorData* data = hw->getTempSensorData(i);
if(data != NULL) {
char* pos = buf+strlen(buf);
snprintf(pos, 26, "\"%s\":%.2f,",
toHex(data->address, 8).c_str(),
data->lastRead
);
data->changed = false;
delay(1);
}
}
char* pos = buf+strlen(buf);
snprintf(count == 0 ? pos : pos-1, 8, "}}");
return mqtt->publish(topic, buf);
}
bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(strlen(eapi->getToken()) == 0)
return false;
time_t now = time(nullptr);
float min1hr, min3hr, min6hr;
int8_t min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1;
float min = INT16_MAX, max = INT16_MIN;
float values[24] = {0};
for(uint8_t i = 0; i < 24; i++) {
float val = eapi->getValueForHour(now, i);
values[i] = val;
if(val == ENTSOE_NO_VALUE) break;
if(val < min) min = val;
if(val > max) max = val;
if(min1hrIdx == -1 || min1hr > val) {
min1hr = val;
min1hrIdx = i;
}
if(i >= 2) {
i -= 2;
float val1 = values[i++];
float val2 = values[i++];
float val3 = val;
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE) continue;
float val3hr = val1+val2+val3;
if(min3hrIdx == -1 || min3hr > val3hr) {
min3hr = val3hr;
min3hrIdx = i-2;
}
}
if(i >= 5) {
i -= 5;
float val1 = values[i++];
float val2 = values[i++];
float val3 = values[i++];
float val4 = values[i++];
float val5 = values[i++];
float val6 = val;
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE || val4 == ENTSOE_NO_VALUE || val5 == ENTSOE_NO_VALUE || val6 == ENTSOE_NO_VALUE) continue;
float val6hr = val1+val2+val3+val4+val5+val6;
if(min6hrIdx == -1 || min6hr > val6hr) {
min6hr = val6hr;
min6hrIdx = i-5;
}
}
}
char ts1hr[21];
if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
//Serial.printf("1hr: %d %lu\n", min1hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts3hr[21];
if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
//Serial.printf("3hr: %d %lu\n", min3hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts6hr[21];
if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
//Serial.printf("6hr: %d %lu\n", min6hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char json[384];
snprintf_P(json, sizeof(json), JSONPRICES_JSON,
WiFi.macAddress().c_str(),
values[0],
values[1],
values[2],
values[3],
values[4],
values[5],
values[6],
values[7],
values[8],
values[9],
values[10],
values[11],
min == INT16_MAX ? 0.0 : min,
max == INT16_MIN ? 0.0 : max,
ts1hr,
ts3hr,
ts6hr
);
return mqtt->publish(topic, json);
}
bool JsonMqttHandler::publishSystem(HwTools* hw) {
if(init || topic.isEmpty() || !mqtt->connected())
return false;
char json[192];
snprintf_P(json, sizeof(json), JSONSYS_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature()
);
init = mqtt->publish(topic, json);
return init;
}

View File

@@ -0,0 +1,24 @@
#ifndef _JSONMQTTHANDLER_H
#define _JSONMQTTHANDLER_H
#include "AmsMqttHandler.h"
class JsonMqttHandler : public AmsMqttHandler {
public:
JsonMqttHandler(MQTTClient* mqtt, const char* clientId, const char* topic, HwTools* hw) : AmsMqttHandler(mqtt) {
this->clientId = clientId;
this->topic = String(topic);
this->hw = hw;
};
bool publish(AmsData* data, AmsData* previousState);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*);
private:
String clientId;
String topic;
HwTools* hw;
bool init = false;
};
#endif

219
src/mqtt/RawMqttHandler.cpp Normal file
View File

@@ -0,0 +1,219 @@
#include "RawMqttHandler.h"
#include "hexutils.h"
#include "Uptime.h"
bool RawMqttHandler::publish(AmsData* data, AmsData* meterState) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(data->getPackageTimestamp() > 0) {
mqtt->publish(topic + "/meter/dlms/timestamp", String(data->getPackageTimestamp()));
}
switch(data->getListType()) {
case 3:
// ID and type belongs to List 2, but I see no need to send that every 10s
mqtt->publish(topic + "/meter/id", data->getMeterId(), true, 0);
mqtt->publish(topic + "/meter/type", data->getMeterModel(), true, 0);
mqtt->publish(topic + "/meter/clock", String(data->getMeterTimestamp()));
mqtt->publish(topic + "/meter/import/reactive/accumulated", String(data->getReactiveImportCounter(), 2), true, 0);
mqtt->publish(topic + "/meter/import/active/accumulated", String(data->getActiveImportCounter(), 2), true, 0);
mqtt->publish(topic + "/meter/export/reactive/accumulated", String(data->getReactiveExportCounter(), 2), true, 0);
mqtt->publish(topic + "/meter/export/active/accumulated", String(data->getActiveExportCounter(), 2), true, 0);
if(full || meterState->getPowerFactor() != data->getPowerFactor()) {
mqtt->publish(topic + "/meter/powerfactor", String(data->getPowerFactor(), 2));
}
if(full || meterState->getL1PowerFactor() != data->getL1PowerFactor()) {
mqtt->publish(topic + "/meter/l1/powerfactor", String(data->getL1PowerFactor(), 2));
}
if(full || meterState->getL2PowerFactor() != data->getL2PowerFactor()) {
mqtt->publish(topic + "/meter/l1/powerfactor", String(data->getL1PowerFactor(), 2));
}
if(full || meterState->getL3PowerFactor() != data->getL3PowerFactor()) {
mqtt->publish(topic + "/meter/l1/powerfactor", String(data->getL1PowerFactor(), 2));
}
case 2:
// Only send data if changed. ID and Type is sent on the 10s interval only if changed
if(full || meterState->getMeterId() != data->getMeterId()) {
mqtt->publish(topic + "/meter/id", data->getMeterId());
}
if(full || meterState->getMeterModel() != data->getMeterModel()) {
mqtt->publish(topic + "/meter/type", data->getMeterModel());
}
if(full || meterState->getL1Current() != data->getL1Current()) {
mqtt->publish(topic + "/meter/l1/current", String(data->getL1Current(), 2));
}
if(full || meterState->getL1Voltage() != data->getL1Voltage()) {
mqtt->publish(topic + "/meter/l1/voltage", String(data->getL1Voltage(), 2));
}
if(full || meterState->getL2Current() != data->getL2Current()) {
mqtt->publish(topic + "/meter/l2/current", String(data->getL2Current(), 2));
}
if(full || meterState->getL2Voltage() != data->getL2Voltage()) {
mqtt->publish(topic + "/meter/l2/voltage", String(data->getL2Voltage(), 2));
}
if(full || meterState->getL3Current() != data->getL3Current()) {
mqtt->publish(topic + "/meter/l3/current", String(data->getL3Current(), 2));
}
if(full || meterState->getL3Voltage() != data->getL3Voltage()) {
mqtt->publish(topic + "/meter/l3/voltage", String(data->getL3Voltage(), 2));
}
if(full || meterState->getReactiveExportPower() != data->getReactiveExportPower()) {
mqtt->publish(topic + "/meter/export/reactive", String(data->getReactiveExportPower()));
}
if(full || meterState->getActiveExportPower() != data->getActiveExportPower()) {
mqtt->publish(topic + "/meter/export/active", String(data->getActiveExportPower()));
}
if(full || meterState->getReactiveImportPower() != data->getReactiveImportPower()) {
mqtt->publish(topic + "/meter/import/reactive", String(data->getReactiveImportPower()));
}
case 1:
if(full || meterState->getActiveImportPower() != data->getActiveImportPower()) {
mqtt->publish(topic + "/meter/import/active", String(data->getActiveImportPower()));
}
}
return true;
}
bool RawMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
uint8_t c = hw->getTempSensorCount();
for(int i = 0; i < c; i++) {
TempSensorData* data = hw->getTempSensorData(i);
if(data != NULL && data->lastValidRead > -85) {
if(data->changed || full) {
mqtt->publish(topic + "/temperature/" + toHex(data->address), String(data->lastValidRead, 2));
data->changed = false;
}
}
}
return c > 0;
}
bool RawMqttHandler::publishPrices(EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(strcmp(eapi->getToken(), "") == 0)
return false;
time_t now = time(nullptr);
float min1hr, min3hr, min6hr;
int8_t min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1;
float min = INT16_MAX, max = INT16_MIN;
float values[34] = {0};
for(uint8_t i = 0; i < 34; i++) {
float val = eapi->getValueForHour(now, i);
values[i] = val;
if(i > 23) continue;
if(val == ENTSOE_NO_VALUE) break;
if(val < min) min = val;
if(val > max) max = val;
if(min1hrIdx == -1 || min1hr > val) {
min1hr = val;
min1hrIdx = i;
}
if(i >= 2) {
i -= 2;
float val1 = values[i++];
float val2 = values[i++];
float val3 = val;
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE) continue;
float val3hr = val1+val2+val3;
if(min3hrIdx == -1 || min3hr > val3hr) {
min3hr = val3hr;
min3hrIdx = i-2;
}
}
if(i >= 5) {
i -= 5;
float val1 = values[i++];
float val2 = values[i++];
float val3 = values[i++];
float val4 = values[i++];
float val5 = values[i++];
float val6 = val;
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE || val4 == ENTSOE_NO_VALUE || val5 == ENTSOE_NO_VALUE || val6 == ENTSOE_NO_VALUE) continue;
float val6hr = val1+val2+val3+val4+val5+val6;
if(min6hrIdx == -1 || min6hr > val6hr) {
min6hr = val6hr;
min6hrIdx = i-5;
}
}
}
char ts1hr[21];
if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
//Serial.printf("1hr: %d %lu\n", min1hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts3hr[21];
if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
//Serial.printf("3hr: %d %lu\n", min3hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts6hr[21];
if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
//Serial.printf("6hr: %d %lu\n", min6hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
for(int i = 0; i < 34; i++) {
float val = values[i];
if(val == ENTSOE_NO_VALUE) {
mqtt->publish(topic + "/price/" + String(i), "", true, 0);
break;
} else {
mqtt->publish(topic + "/price/" + String(i), String(val, 4), true, 0);
}
mqtt->loop();
delay(10);
}
if(min != INT16_MAX) {
mqtt->publish(topic + "/price/min", String(min, 4), true, 0);
}
if(max != INT16_MIN) {
mqtt->publish(topic + "/price/max", String(max, 4), true, 0);
}
if(min1hrIdx != -1) {
mqtt->publish(topic + "/price/cheapest/1hr", String(ts1hr), true, 0);
}
if(min3hrIdx != -1) {
mqtt->publish(topic + "/price/cheapest/3hr", String(ts3hr), true, 0);
}
if(min6hrIdx != -1) {
mqtt->publish(topic + "/price/cheapest/6hr", String(ts6hr), true, 0);
}
return true;
}
bool RawMqttHandler::publishSystem(HwTools* hw) {
if(topic.isEmpty() || !mqtt->connected())
return false;
mqtt->publish(topic + "/id", WiFi.macAddress(), true, 0);
mqtt->publish(topic + "/uptime", String((unsigned long) millis64()/1000));
float vcc = hw->getVcc();
if(vcc > 0) {
mqtt->publish(topic + "/vcc", String(vcc, 2));
}
mqtt->publish(topic + "/rssi", String(hw->getWifiRssi()));
if(hw->getTemperature() > -85) {
mqtt->publish(topic + "/temperature", String(hw->getTemperature(), 2));
}
return true;
}

21
src/mqtt/RawMqttHandler.h Normal file
View File

@@ -0,0 +1,21 @@
#ifndef _RAWMQTTHANDLER_H
#define _RAWMQTTHANDLER_H
#include "AmsMqttHandler.h"
class RawMqttHandler : public AmsMqttHandler {
public:
RawMqttHandler(MQTTClient* mqtt, const char* topic, bool full) : AmsMqttHandler(mqtt) {
this->topic = String(topic);
this->full = full;
};
bool publish(AmsData* data, AmsData* previousState);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*);
private:
String topic;
bool full;
};
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -3,48 +3,56 @@
#define BOOTSTRAP_URL "https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css"
#include "Arduino.h"
#include <MQTT.h>
#include <ArduinoJson.h>
#include "AmsConfiguration.h"
#include "HwTools.h"
#include "AmsData.h"
#include "AmsDataStorage.h"
#include "Uptime.h"
#include "RemoteDebug.h"
#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif
#include "entsoe/EntsoeApi.h"
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266HTTPClient.h>
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
#include <WiFi.h>
#include <WebServer.h>
#include "SPIFFS.h"
#include "Update.h"
#include <HTTPClient.h>
#else
#warning "Unsupported board type"
#endif
#include "LittleFS.h"
class AmsWebServer {
public:
AmsWebServer(RemoteDebug* Debug);
void setup(AmsConfiguration* config, MQTTClient* mqtt);
AmsWebServer(RemoteDebug* Debug, HwTools* hw);
void setup(AmsConfiguration*, GpioConfig*, MeterConfig*, AmsData*, AmsDataStorage*);
void loop();
void setData(AmsData& data);
void setMqtt(MQTTClient* mqtt);
void setTimezone(Timezone* tz);
void setMqttEnabled(bool);
void setEntsoeApi(EntsoeApi* eapi);
private:
RemoteDebug* debugger;
bool mqttEnabled = false;
int maxPwr = 0;
HwTools hw;
AmsConfiguration* config;
AmsData data;
MQTTClient* mqtt;
File firmwareFile;
HwTools* hw;
Timezone* tz;
EntsoeApi* eapi = NULL;
AmsConfiguration* config;
GpioConfig* gpioConfig;
MeterConfig* meterConfig;
WebConfig webConfig;
AmsData* meterState;
AmsDataStorage* ds;
MQTTClient* mqtt = NULL;
bool uploading = false;
File file;
bool performRestart = false;
#if defined(ESP8266)
@@ -56,22 +64,59 @@ private:
bool checkSecurity(byte level);
void indexHtml();
void applicationJs();
void temperature();
void temperaturePost();
void temperatureJson();
void price();
void configMeterHtml();
void configWifiHtml();
void configMqttHtml();
void configWebHtml();
void configDomoticzHtml();
void configEntsoeHtml();
void configNtpHtml();
void configGpioHtml();
void configDebugHtml();
void bootCss();
void gaugemeterJs();
void githubSvg();
void dataJson();
void dayplotJson();
void monthplotJson();
void energyPriceJson();
void handleSetup();
void handleSave();
void configSystemHtml();
void configSystemPost();
void configSystemUpload();
String getSerialSelectOptions(int selected);
void firmwareHtml();
void firmwareUpload();
void firmwareDownload();
void restartHtml();
void restartPost();
void restartWaitHtml();
void isAliveCheck();
void uploadHtml(const char* label, const char* action, const char* menu);
void deleteHtml(const char* label, const char* action, const char* menu);
void uploadFile(const char* path);
void deleteFile(const char* path);
void uploadPost();
void mqttCa();
void mqttCaUpload();
void mqttCaDelete();
void mqttCert();
void mqttCertUpload();
void mqttCertDelete();
void mqttKey();
void mqttKeyUpload();
void mqttKeyDelete();
void factoryResetHtml();
void factoryResetPost();
void notFound();
void printD(String fmt, ...);
void printI(String fmt, ...);
void printW(String fmt, ...);

739
web/application.js Normal file
View File

@@ -0,0 +1,739 @@
var nextVersion;
var im, em;
// Price plot
var pp;
var pa;
var po = {
title: 'Future energy price',
titleTextStyle: {
fontSize: 14
},
bar: { groupWidth: '90%' },
legend: { position: 'none' },
vAxis: {
viewWindowMode: 'maximized'
},
tooltip: { trigger: 'none'},
enableInteractivity: false,
};
var pl = null; // Last price
// Day plot
var ep;
var ea;
var eo = {
title: 'Energy use last 24 hours',
titleTextStyle: {
fontSize: 14
},
bar: { groupWidth: '90%' },
legend: { position: 'none' },
vAxis: {
title: 'kWh',
viewWindowMode: 'maximized'
},
tooltip: { trigger: 'none'},
enableInteractivity: false,
};
// Month plot
var mp;
var ma;
var mo = {
title: 'Energy use last month',
titleTextStyle: {
fontSize: 14
},
bar: { groupWidth: '90%' },
legend: { position: 'none' },
vAxis: {
title: 'kWh',
viewWindowMode: 'maximized'
},
tooltip: { trigger: 'none'},
enableInteractivity: false,
};
// Voltage plot
var vp;
var va;
var vo = {
title: 'Phase voltage',
titleTextStyle: {
fontSize: 14
},
bar: { groupWidth: '90%' },
vAxis: {
minValue: 200,
maxValue: 260,
ticks: [
{ v: 207, f: '-10%'},
{ v: 230, f: '230V'},
{ v: 253, f: '+10%'}
]
},
legend: { position: 'none' },
tooltip: { trigger: 'none'},
enableInteractivity: false,
};
// Amperage plot
var ap;
var aa;
var ao = {
title: 'Phase current',
titleTextStyle: {
fontSize: 14
},
bar: { groupWidth: '90%' },
vAxis: {
minValue: 0,
maxValue: 100,
ticks: [
{ v: 25, f: '25%'},
{ v: 50, f: '50%'},
{ v: 75, f: '75%'},
{ v: 100, f: '100%'}
]
},
legend: { position: 'none' },
tooltip: { trigger: 'none'},
enableInteractivity: false,
};
// Import plot
var ip;
var ia;
var io = {
legend: 'none',
pieHole: 0.6,
pieSliceText: 'none',
pieStartAngle: 216,
slices: {
0: { color: 'green' },
1: { color: '#eee' },
2: { color: 'transparent' }
},
legend: { position: 'none' },
tooltip: { trigger: 'none'},
enableInteractivity: false,
chartArea: {
left: 0,
top: 0,
width: '100%',
height: '100%'
}
};
// Export plot
var xp;
var xa;
var xo = {
legend: 'none',
pieHole: 0.6,
pieSliceText: 'none',
pieStartAngle: 216,
slices: {
0: { color: 'green' },
1: { color: '#eee' },
2: { color: 'transparent' }
},
legend: { position: 'none' },
tooltip: { trigger: 'none'},
enableInteractivity: false,
chartArea: {
left: 0,
top: 0,
width: '100%',
height: '100%'
}
};
$(function() {
var meters = $('.plot1');
if(meters.length > 0) {
// Chart
google.charts.load('current', {'packages':['corechart']});
google.charts.setOnLoadCallback(setupChart);
}
// For mqtt
$('#m').on('change', function() {
var inputs = $('.mc');
inputs.prop('disabled', !$(this).is(':checked'));
});
$('#f').on('change', function() {
var val = parseInt($(this).val());
if(val == 3) {
$('.f3-s').show();
} else {
$('.f3-s').hide();
}
});
$('#s').on('change', function() {
var port = $('#p');
if($(this).is(':checked')) {
if(port.val() == 1883) {
port.val(8883);
}
} else {
if(port.val() == 8883) {
port.val(1883);
}
}
});
$('#m').trigger('change');
$('#f').trigger('change');
// For wifi
$('#st').on('change', function() {
if($(this).is(':checked')) {
$('#i').show();
} else {
$('#i').hide();
}
});
$('#st').trigger('change');
// For web
$('#as').on('change', function() {
var inputs = $('.ac');
inputs.prop('disabled', $(this).val() == 0);
});
$('#as').trigger('change');
// For file upload
$('#fileUploadField').on('change',function(){
var fileName = $(this).val();
$(this).next('.custom-file-label').html(fileName);
})
$('.upload-form').on('submit', function(i, form) {
$('#loading-indicator').show();
});
// For NTP
$('#n').on('change', function() {
var inputs = $('.nc');
inputs.prop('disabled', !$(this).is(':checked'));
});
$('#n').trigger('change');
// Navbar
switch(window.location.pathname) {
case '/temperature':
$('#temp-link').addClass('active');
break;
case '/price':
$('#price-link').addClass('active');
break;
case '/meter':
case '/wifi':
case '/mqtt':
case '/mqtt-ca':
case '/mqtt-cert':
case '/mqtt-key':
case '/domoticz':
case '/web':
case '/ntp':
case '/entsoe':
$('#config-link').addClass('active');
break;
case '/gpio':
case '/debugging':
case '/firmware':
$('#firmware-warn').show();
case '/reset':
$('#system-link').addClass('active');
break;
}
// Check for software upgrade
var swv = $('#swVersion');
var fwl = $('#fwLink');
if((meters.length > 0 || fwl.length > 0) && swv.length == 1) {
var v = swv.text().substring(1).split('.');
var v_major = parseInt(v[0]);
var v_minor = parseInt(v[1]);
var v_patch = parseInt(v[2]);
$.ajax({
url: swv.data('url'),
dataType: 'json'
}).done(function(releases) {
if(!swv.text().match("^v\d{1,2}\.\d{1,2}\.\d{1,2}$")) {
nextVersion = releases[0];
} else {
releases.reverse();
var next_patch;
var next_minor;
var next_major;
$.each(releases, function(i, release) {
var ver2 = release.tag_name;
var v2 = ver2.substring(1).split('.');
var v2_major = parseInt(v2[0]);
var v2_minor = parseInt(v2[1]);
var v2_patch = parseInt(v2[2]);
if(v2_major == v_major) {
if(v2_minor == v_minor) {
if(v2_patch > v_patch) {
next_patch = release;
}
} else if(v2_minor == v_minor+1) {
next_minor = release;
}
} else if(v2_major == v_major+1) {
if(next_major) {
var mv = next_major.tag_name.substring(1).split('.');
var mv_major = parseInt(mv[0]);
var mv_minor = parseInt(mv[1]);
var mv_patch = parseInt(mv[2]);
if(v2_minor == mv_minor) {
next_major = release;
}
} else {
next_major = release;
}
}
});
if(next_minor) {
nextVersion = next_minor;
} else if(next_major) {
nextVersion = next_major;
} else if(next_patch) {
nextVersion = next_patch;
}
}
if(nextVersion) {
if(fwl.length > 0) {
var chipset = fwl.data('chipset').toLowerCase();
$.each(releases, function(i, release) {
if(release.tag_name == nextVersion.tag_name) {
$.each(release.assets, function(i, asset) {
if(asset.name.includes(chipset) && !asset.name.includes("partitions")) {
fwl.prop('href', asset.browser_download_url);
$('#fwDownload').show();
return false;
}
});
}
});
};
$('#newVersionTag').text(nextVersion.tag_name);
$('#newVersionUrl').prop('href', nextVersion.html_url);
$('#newVersion').removeClass('d-none');
}
});
}
// Temperature
var tt = $('#temp-template');
if(tt.length > 0) {
setTimeout(loadTempSensors, 500);
}
});
var resizeTO;
$( window ).resize(function() {
if(resizeTO) clearTimeout(resizeTO);
resizeTO = setTimeout(function() {
$(this).trigger('resizeEnd');
}, 250);
});
$(window).on('resizeEnd', function() {
redraw();
});
var zeropad = function(num) {
num = num.toString();
while (num.length < 2) num = "0" + num;
return num;
}
var setupChart = function() {
pp = new google.visualization.ColumnChart(document.getElementById('pp'));
ep = new google.visualization.ColumnChart(document.getElementById('ep'));
mp = new google.visualization.ColumnChart(document.getElementById('mp'));
vp = new google.visualization.ColumnChart(document.getElementById('vp'));
ap = new google.visualization.ColumnChart(document.getElementById('ap'));
ip = new google.visualization.PieChart(document.getElementById('ip'));
xp = new google.visualization.PieChart(document.getElementById('xp'));
fetch();
drawDay();
drawMonth();
};
var redraw = function() {
if(pl != null) {
pp.draw(pa, po);
}
ep.draw(ea, eo);
mp.draw(ma, mo);
vp.draw(va, vo);
ap.draw(aa, ao);
ip.draw(ia, io);
xp.draw(xa, xo);
};
var drawPrices = function() {
$('#ppc').show();
$.ajax({
url: '/energyprice.json',
timeout: 30000,
dataType: 'json',
}).done(function(json) {
data = [['Hour',json.currency + '/kWh', { role: 'style' }, { role: 'annotation' }]];
var r = 1;
var hour = moment.utc().hours();
var offset = moment().utcOffset()/60;
var min = 0;
var h = 0;
var d = json["20"] == null ? 2 : 1;
for(var i = hour; i<24; i++) {
var val = json[zeropad(h++)];
if(val == null) break;
data[r++] = [zeropad((i+offset)%24), val, "color: #6f42c1;opacity: 0.9;", val == null ? "" : val.toFixed(d)];
Math.min(0, val);
};
for(var i = 0; i < 24; i++) {
var val = json[zeropad(h++)];
if(val == null) break;
data[r++] = [zeropad((i+offset)%24), val, "color: #6f42c1;opacity: 0.9;", val == null ? "" : val.toFixed(d)];
Math.min(0, val);
};
pa = google.visualization.arrayToDataTable(data);
po.vAxis.title = json.currency;
if(min == 0)
po.vAxis.minValue = 0;
pp.draw(pa, po);
});
}
var drawDay = function() {
$.ajax({
url: '/dayplot.json',
timeout: 30000,
dataType: 'json',
}).done(function(json) {
data = [['Hour','kWh', { role: 'style' }, { role: 'annotation' }]];
var r = 1;
var hour = moment.utc().hours();
var offset = moment().utcOffset()/60;
var min = 0;
for(var i = hour; i<24; i++) {
var val = json["h"+zeropad(i)];
data[r++] = [zeropad((i+offset)%24), val, "color: #6f42c1;opacity: 0.9;", val.toFixed(1)];
Math.min(0, val);
};
for(var i = 0; i < hour; i++) {
var val = json["h"+zeropad(i)];
data[r++] = [zeropad((i+offset)%24), val, "color: #6f42c1;opacity: 0.9;", val.toFixed(1)];
Math.min(0, val);
};
ea = google.visualization.arrayToDataTable(data);
if(min == 0)
eo.vAxis.minValue = 0;
ep.draw(ea, eo);
setTimeout(drawDay, (61-moment().minute())*60000);
});
};
var drawMonth = function() {
$.ajax({
url: '/monthplot.json',
timeout: 30000,
dataType: 'json',
}).done(function(json) {
data = [['Day','kWh', { role: 'style' }, { role: 'annotation' }]];
var r = 1;
var day = moment().date();
var eom = moment().subtract(1, 'months').endOf('month').date();
var min = 0;
for(var i = day; i<=eom; i++) {
var val = json["d"+zeropad(i)];
data[r++] = [zeropad((i)), val, "color: #6f42c1;opacity: 0.9;", val.toFixed(0)];
Math.min(0, val);
}
for(var i = 1; i < day; i++) {
var val = json["d"+zeropad(i)];
data[r++] = [zeropad((i)), val, "color: #6f42c1;opacity: 0.9;", val.toFixed(0)];
Math.min(0, val);
}
ma = google.visualization.arrayToDataTable(data);
if(min == 0)
mo.vAxis.minValue = 0;
mp.draw(ma, mo);
setTimeout(drawMonth, (24-moment().hour())*60000);
});
};
var setStatus = function(id, sid) {
var item = $('#'+id);
item.removeClass('d-none');
item.removeClass (function (index, className) {
return (className.match (/(^|\s)badge-\S+/g) || []).join(' ');
});
var status;
if(sid == 0) {
status = "secondary";
} else if(sid == 1) {
status = "success";
} else if(sid == 2) {
status = "warning";
} else {
status = "danger";
}
item.addClass('badge badge-' + status);
};
var voltcol = function(pct) {
if(pct > 85) return '#d90000';
else if(pct > 75) return'#e32100';
else if(pct > 70) return '#ffb800';
else if(pct > 65) return '#dcd800';
else if(pct > 35) return '#32d900';
else if(pct > 25) return '#dcd800';
else if(pct > 20) return '#ffb800';
else if(pct > 15) return'#e32100';
else return '#d90000';
};
var ampcol = function(pct) {
if(pct > 85) return '#d90000';
else if(pct > 75) return'#e32100';
else if(pct > 70) return '#ffb800';
else if(pct > 65) return '#dcd800';
else return '#32d900';
};
var interval = 5000;
var fetch = function() {
$.ajax({
url: '/data.json',
timeout: 10000,
dataType: 'json',
}).done(function(json) {
if(im) {
$(".SimpleMeter").hide();
im.show();
em.show();
}
for(var id in json) {
var str = json[id];
if(typeof str === "object") {
continue;
}
if(isNaN(str)) {
$('.j'+id).html(str);
} else {
var num = parseFloat(str);
$('.j'+id).html(num.toFixed(num < 0 ? 0 : num < 10 ? 2 : 1));
}
$('.r'+id).show();
}
if(window.moment) {
$('.ju').html(moment.duration(parseInt(json.u), 'seconds').humanize());
}
var kib = parseInt(json.m)/1000;
$('.jm').html(kib.toFixed(1));
if(kib > 32) {
$('.ssl-capable').removeClass('d-none');
}
setStatus("esp", json.em);
setStatus("han", json.hm);
setStatus("wifi", json.wm);
setStatus("mqtt", json.mm);
if(ip) {
var v = parseInt(json.i);
var pct = (v*100)/parseInt(json.im);
var append = "W";
if(v > 1000) {
v = (v/1000).toFixed(1);
append = "kW";
}
$('.ipo').html(v);
$('.ipoa').html(append);
var arr = [
['Slice', 'Value'],
['', (pct*2.88)],
['', ((100-pct)*2.88)],
['', 72],
];
io.slices[0].color = ampcol(pct);
ia = google.visualization.arrayToDataTable(arr);
ip.draw(ia, io);
}
if(xp) {
var v = parseInt(json.e);
var pct = (v*100)/(parseInt(json.om)*1000);
var append = "W";
if(v > 1000) {
v = (v/1000).toFixed(1);
append = "kW";
}
$('.epo').html(v);
$('.epoa').html(append);
var arr = [
['Slice', 'Value'],
['', (pct*2.88)],
['', ((100-pct)*2.88)],
['', 72],
];
xo.slices[0].color = ampcol(pct);
xa = google.visualization.arrayToDataTable(arr);
xp.draw(xa, xo);
}
if(vp) {
var c = 0;
var t = 0;
var r = 1;
var arr = [['Phase', 'Voltage', { role: 'style' }, { role: 'annotation' }]];
if(json.u1) {
var u1 = parseFloat(json.u1);
t += u1;
c++;
var pct = (Math.max(parseFloat(json.u1)-195.5, 1)*100/69);
arr[r++] = ['L1', u1, "color: " + voltcol(pct) + ";opacity: 0.9;", u1 + "V"];
}
if(json.u2) {
var u2 = parseFloat(json.u2);
t += u2;
c++;
var pct = (Math.max(parseFloat(json.u2)-195.5, 1)*100/69);
arr[r++] = ['L2', u2, "color: " + voltcol(pct) + ";opacity: 0.9;", u2 + "V"];
}
if(json.u3) {
var u3 = parseFloat(json.u3);
t += u3;
c++;
var pct = (Math.max(parseFloat(json.u3)-195.5, 1)*100/69);
arr[r++] = ['L3', u3, "color: " + voltcol(pct) + ";opacity: 0.9;", u3 + "V"];
}
v = t/c;
if(v > 0) {
va = google.visualization.arrayToDataTable(arr);
vp.draw(va, vo);
}
}
if(ap && json.mf) {
var a = 0;
var r = 1;
var arr = [['Phase', 'Amperage', { role: 'style' }, { role: 'annotation' }]];
if(json.i1) {
var i1 = parseFloat(json.i1);
a = Math.max(a, i1);
var pct = (parseFloat(json.i1)/parseInt(json.mf))*100;
arr[r++] = ['L1', pct, "color: " + ampcol(pct) + ";opacity: 0.9;", i1 + "A"];
}
if(json.i2) {
var i2 = parseFloat(json.i2);
a = Math.max(a, i2);
var pct = (parseFloat(json.i2)/parseInt(json.mf))*100;
arr[r++] = ['L2', pct, "color: " + ampcol(pct) + ";opacity: 0.9;", i2 + "A"];
}
if(json.i3) {
var i3 = parseFloat(json.i3);
a = Math.max(a, i3);
var pct = (parseFloat(json.i3)/parseInt(json.mf))*100;
arr[r++] = ['L3', pct, "color: " + ampcol(pct) + ";opacity: 0.9;", i3 + "A"];
}
if(a > 0) {
aa = google.visualization.arrayToDataTable(arr);
ap.draw(aa, ao);
}
}
if(json.me) {
$('.me').addClass('d-none');
$('.me'+json.me).removeClass('d-none');
$('#ml').html(json.me);
}
var temp = parseInt(json.t);
if(temp == -127) {
$('.jt').html("N/A");
}
setTimeout(fetch, interval);
var price = parseFloat(json.p);
if(price && price != pl) {
pl = price;
drawPrices();
}
}).fail(function(x, text, error) {
console.log("Failed request");
console.log(text);
console.log(error);
setTimeout(fetch, interval*4);
setStatus("mqtt", 0);
setStatus("wifi", 0);
setStatus("han", 0);
setStatus("esp", 3);
});
}
var upgrade = function() {
if(nextVersion) {
if(confirm("Are you sure you want to perform upgrade to " + nextVersion.tag_name + "?")) {
$('#loading-indicator').show();
window.location.href="/upgrade?version=" + nextVersion.tag_name;
}
}
}
var loadTempSensors = function() {
$.ajax({
url: '/temperature.json',
timeout: 10000,
dataType: 'json',
}).done(function(json) {
if($('#loading').length > 0) {
$('#loading').hide();
var list = $('#sensors');
if(json.c > 0) {
list.empty();
var temp = $.trim($('#temp-template').html());
$.each(json.s, function(i, o) {
var item = temp.replace(/{{index}}/ig, o.i);
var item = item.replace(/{{address}}/ig, o.a);
var item = item.replace(/{{name}}/ig, o.n);
var item = item.replace(/{{value}}/ig, o.v > -50 && o.v < 127 ? o.v : "N/A");
var item = item.replace(/{{common}}/ig, o.c ? "checked" : "");
list.append(item);
});
} else {
$('#notemp').show();
}
} else {
$.each(json.s, function(i, o) {
$('#temp-'+o.i).html(o.v > -50 && o.v < 127 ? o.v : "N/A");
});
}
setTimeout(loadTempSensors, 10000);
}).fail(function() {
setTimeout(loadTempSensors, 60000);
$('#loading').hide();
$('#error').show();
});
}

View File

@@ -56,24 +56,9 @@ body {
.bg-purple {
background-color: var(--purple);
}
.text-white-50 {
color: rgba(255,255,255,.5)!important;
}
.mb-0, .my-0 {
margin-bottom: 0!important;
}
.m-2 {
margin: .5rem!important;
}
.mb-2, .my-2 {
margin-bottom: .5rem!important;
}
.mt-2, .my-2 {
margin-top: .5rem!important;
}
.pb-2, .py-2 {
padding-bottom: .5rem!important;
}
.p-3 {
padding: 1rem!important;
}
@@ -89,19 +74,9 @@ body {
.shadow {
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
}
.align-items-center {
-ms-flex-align: center!important;
align-items: center!important;
}
.border-bottom {
border-bottom: 1px solid #dee2e6!important;
}
.rounded {
border-radius: .25rem!important;
}
div {
display: block;
}
.container {
width: 100%;
padding-right: 15px;
@@ -115,15 +90,9 @@ article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
main {
display: block;
}
.text-white {
color: #fff!important;
}
.text-right {
text-align: right!important;
}
.text-center {
text-align: center!important;
}
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
margin-bottom: .5rem;
font-weight: 500;
@@ -133,6 +102,9 @@ h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: .5rem;
}
.h5, h5 {
font-size: 1.25rem;
}
.h6, h6 {
font-size: 1rem;
}
@@ -150,48 +122,14 @@ h1, h2, h3, h4, h5, h6 {
padding-right: 15px;
padding-left: 15px;
}
.col-2 {
-ms-flex: 0 0 16.666667%;
flex: 0 0 16.666667%;
max-width: 16.666667%;
}
.col-3 {
-ms-flex: 0 0 25%;
flex: 0 0 25%;
max-width: 25%;
}
.col-4 {
-ms-flex: 0 0 33.333333%;
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
.col-5 {
-ms-flex: 0 0 41.666667%;
flex: 0 0 41.666667%;
max-width: 41.666667%;
}
.col-6 {
-ms-flex: 0 0 50%;
flex: 0 0 50%;
max-width: 50%;
}
.col-8 {
-ms-flex: 0 0 66.666667%;
flex: 0 0 66.666667%;
max-width: 66.666667%;
}
.col-9 {
-ms-flex: 0 0 75%;
flex: 0 0 75%;
max-width: 75%;
}
.d-none {
display: none!important;
}
.d-flex {
display: -ms-flexbox!important;
display: flex!important;
}
.flex-row {
-ms-flex-direction: row!important;
flex-direction: row!important;
@@ -225,9 +163,11 @@ a {
border-radius: .25rem;
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.btn-outline-secondary {
color: #6c757d;
border-color: #6c757d;
.btn-group-sm>.btn, .btn-sm {
padding: .25rem .5rem;
font-size: .875rem;
line-height: 1.5;
border-radius: .2rem;
}
.btn-primary {
color: #fff;
@@ -295,46 +235,6 @@ a {
.navbar-dark .navbar-nav .active>.nav-link, .navbar-dark .navbar-nav .nav-link.active, .navbar-dark .navbar-nav .nav-link.show, .navbar-dark .navbar-nav .show>.nav-link {
color: #fff;
}
.badge {
display: inline-block;
padding: .25em .4em;
font-size: 75%;
font-weight: 700;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: .25rem;
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.badge-secondary {
color: #fff;
background-color: #6c757d;
}
.badge-success {
color: #fff;
background-color: #28a745;
}
.badge-warning {
color: #212529;
background-color: #ffc107;
}
.badge-danger {
color: #fff;
background-color: #dc3545;
}
.alert {
position: relative;
padding: .75rem 1.25rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: .25rem;
}
.alert-warning {
color: #856404;
background-color: #fff3cd;
border-color: #ffeeba;
}
.form-group {
margin-bottom: 1rem;
}
@@ -386,6 +286,9 @@ input[type="radio"], input[type="checkbox"] {
.input-group-append {
margin-left: -1px;
}
.input-group-prepend {
margin-right: -1px;
}
.input-group-append, .input-group-prepend {
display: -ms-flexbox;
display: flex;
@@ -394,6 +297,14 @@ input[type="radio"], input[type="checkbox"] {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle), .input-group>.input-group-append:last-child>.input-group-text:not(:last-child), .input-group>.input-group-append:not(:last-child)>.btn, .input-group>.input-group-append:not(:last-child)>.input-group-text, .input-group>.input-group-prepend>.btn, .input-group>.input-group-prepend>.input-group-text {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group>.custom-select:not(:first-child), .input-group>.form-control:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.input-group-text {
display: -ms-flexbox;
display: flex;
@@ -453,6 +364,11 @@ dl, ol, ul {
.container, .container-sm {
max-width: 540px;
}
.col-sm-6 {
-ms-flex: 0 0 50%;
flex: 0 0 50%;
max-width: 50%;
}
}
@media (min-width: 768px) {
.container, .container-md, .container-sm {
@@ -463,11 +379,6 @@ dl, ol, ul {
flex: 0 0 25%;
max-width: 25%;
}
.col-md-4 {
-ms-flex: 0 0 33.333333%;
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
.col-md-6 {
-ms-flex: 0 0 50%;
flex: 0 0 50%;
@@ -499,6 +410,21 @@ dl, ol, ul {
.container, .container-lg, .container-md, .container-sm, .container-xl {
max-width: 1140px;
}
.col-xl-2 {
-ms-flex: 0 0 16.666667%;
flex: 0 0 16.666667%;
max-width: 16.666667%;
}
.col-xl-3 {
-ms-flex: 0 0 25%;
flex: 0 0 25%;
max-width: 25%;
}
.col-xl-4 {
-ms-flex: 0 0 33.333333%;
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
}
*, ::after, ::before {

View File

@@ -1,106 +0,0 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AMS reader - Meter configuration</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" type="text/css" href="boot.css"/>
</head>
<body class="bg-light">
<main role="main" class="container">
<header class="navbar navbar-expand navbar-dark flex-column flex-md-row bg-purple rounded mt-2 mb-4" style="background-color: var(--purple);">
<a href="/" class=""><h6 class="navbar-brand">AMS reader <small>${version}</small></h6></a>
<div class="navbar-nav-scroll">
<ul class="navbar-nav bd-navbar-nav flex-row">
<li class="nav-item">
<a class="nav-link active" href="/config-meter">Meter</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config-wifi">WiFi</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config-mqtt">MQTT</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config-web">Web</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config-system">System</a>
</li>
</ul>
</div>
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
<li class="nav-item">
<a class="nav-link p-2" href="https://github.com/gskjold/AmsToMqttBridge" target="_blank" rel="noopener" aria-label="GitHub">
<svg class="d-inline-block align-text-top" style="width: 2rem; height: 2rem;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 499.36" focusable="false"><title>GitHub</title><path d="M256 0C114.64 0 0 114.61 0 256c0 113.09 73.34 209 175.08 242.9 12.8 2.35 17.47-5.56 17.47-12.34 0-6.08-.22-22.18-.35-43.54-71.2 15.49-86.2-34.34-86.2-34.34-11.64-29.57-28.42-37.45-28.42-37.45-23.27-15.84 1.73-15.55 1.73-15.55 25.69 1.81 39.21 26.38 39.21 26.38 22.84 39.12 59.92 27.82 74.5 21.27 2.33-16.54 8.94-27.82 16.25-34.22-56.84-6.43-116.6-28.43-116.6-126.49 0-27.95 10-50.8 26.35-68.69-2.63-6.48-11.42-32.5 2.51-67.75 0 0 21.49-6.88 70.4 26.24a242.65 242.65 0 0 1 128.18 0c48.87-33.13 70.33-26.24 70.33-26.24 14 35.25 5.18 61.27 2.55 67.75 16.41 17.9 26.31 40.75 26.31 68.69 0 98.35-59.85 120-116.88 126.32 9.19 7.9 17.38 23.53 17.38 47.41 0 34.22-.31 61.83-.31 70.23 0 6.85 4.61 14.81 17.6 12.31C438.72 464.97 512 369.08 512 256.02 512 114.62 397.37 0 256 0z" fill="currentColor" fill-rule="evenodd"></path></svg>
</a>
</li>
</ul>
</header>
<form method="post" action="/save">
<input type="hidden" name="meterConfig" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<div class="row">
<div class="col-md-4">
<div class="row form-group">
<label class="col-6">Meter type</label>
<div class="col-6">
<select class="form-control" name="meterType">
<option value="0" ${config.meterType0}>Autodetect</option>
<option value="1" ${config.meterType1}>Kaifa</option>
<option value="2" ${config.meterType2}>Aidon</option>
<option value="3" ${config.meterType3}>Kamstrup</option>
</select>
</div>
</div>
<div class="row form-group">
<label class="col-6">Distribution system</label>
<div class="col-6">
<select class="form-control" name="distributionSystem">
<option value="0" ${config.distributionSystem0}></option>
<option value="1" ${config.distributionSystem1}>IT (230V)</option>
<option value="2" ${config.distributionSystem2}>TN (400V)</option>
</select>
</div>
</div>
</div>
<div class="col-md-4">
<div class="row form-group">
<label class="col-6">Main fuse</label>
<div class="col-6">
<select class="form-control" name="mainFuse">
<option value="0" ${config.mainFuse0}></option>
<option value="25" ${config.mainFuse25}>25A</option>
<option value="32" ${config.mainFuse32}>32A</option>
<option value="35" ${config.mainFuse32}>35A</option>
<option value="40" ${config.mainFuse40}>40A</option>
<option value="50" ${config.mainFuse50}>50A</option>
<option value="63" ${config.mainFuse63}>63A</option>
</select>
</div>
</div>
<div class="row form-group">
<label class="col-6">Production capacity</label>
<div class="col-6">
<div class="input-group">
<input class="form-control" name="productionCapacity" type="number" min="0" max="50" value="${config.productionCapacity}"/>
<div class="input-group-append"><span class="input-group-text">kWp</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>
</main>
</body>
</html>

View File

@@ -1,128 +0,0 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AMS reader - WiFi configuration</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" type="text/css" href="boot.css"/>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
</head>
<body class="bg-light">
<main role="main" class="container">
<header class="navbar navbar-expand navbar-dark flex-column flex-md-row bg-purple rounded mt-2 mb-4" style="background-color: var(--purple);">
<a href="/" class=""><h6 class="navbar-brand">AMS reader <small>${version}</small></h6></a>
<div class="navbar-nav-scroll">
<ul class="navbar-nav bd-navbar-nav flex-row">
<li class="nav-item">
<a class="nav-link" href="/config-meter">Meter</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config-wifi">WiFi</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/config-mqtt">MQTT</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config-web">Web</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config-system">System</a>
</li>
</ul>
</div>
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
<li class="nav-item">
<a class="nav-link p-2" href="https://github.com/gskjold/AmsToMqttBridge" target="_blank" rel="noopener" aria-label="GitHub">
<svg class="d-inline-block align-text-top" style="width: 2rem; height: 2rem;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 499.36" focusable="false"><title>GitHub</title><path d="M256 0C114.64 0 0 114.61 0 256c0 113.09 73.34 209 175.08 242.9 12.8 2.35 17.47-5.56 17.47-12.34 0-6.08-.22-22.18-.35-43.54-71.2 15.49-86.2-34.34-86.2-34.34-11.64-29.57-28.42-37.45-28.42-37.45-23.27-15.84 1.73-15.55 1.73-15.55 25.69 1.81 39.21 26.38 39.21 26.38 22.84 39.12 59.92 27.82 74.5 21.27 2.33-16.54 8.94-27.82 16.25-34.22-56.84-6.43-116.6-28.43-116.6-126.49 0-27.95 10-50.8 26.35-68.69-2.63-6.48-11.42-32.5 2.51-67.75 0 0 21.49-6.88 70.4 26.24a242.65 242.65 0 0 1 128.18 0c48.87-33.13 70.33-26.24 70.33-26.24 14 35.25 5.18 61.27 2.55 67.75 16.41 17.9 26.31 40.75 26.31 68.69 0 98.35-59.85 120-116.88 126.32 9.19 7.9 17.38 23.53 17.38 47.41 0 34.22-.31 61.83-.31 70.23 0 6.85 4.61 14.81 17.6 12.31C438.72 464.97 512 369.08 512 256.02 512 114.62 397.37 0 256 0z" fill="currentColor" fill-rule="evenodd"></path></svg>
</a>
</li>
</ul>
</header>
<form method="post" action="/save">
<input type="hidden" name="mqttConfig" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<div class="row">
<div class="col-md-3">
<div class="row form-group">
<label class="col-4">Enable</label>
<div class="col-8">
<input id="mqttEnable" type="checkbox" name="mqtt" value="true" ${config.mqtt}/>
</div>
</div>
<div class="row form-group">
<label class="col-4">Payload</label>
<div class="col-8">
<select class="form-control mqtt-config" name="mqttPayloadFormat">
<option value="0" ${config.mqttPayloadFormat0}>JSON</option>
<option value="1" ${config.mqttPayloadFormat1}>Raw</option>
</select>
</div>
</div>
</div>
<div class="col-md-3">
<div class="row form-group">
<label class="col-4">Hostname</label>
<div class="col-8">
<input type="text" class="form-control mqtt-config" name="mqttHost" value="${config.mqttHost}"/>
</div>
</div>
<div class="row form-group">
<label class="col-4">Port</label>
<div class="col-8">
<input type="text" class="form-control mqtt-config" name="mqttPort" value="${config.mqttPort}"/>
</div>
</div>
</div>
<div class="col-md-3">
<div class="row form-group">
<label class="col-4">Client ID</label>
<div class="col-8">
<input type="text" class="form-control mqtt-config" name="mqttClientId" value="${config.mqttClientId}"/>
</div>
</div>
<div class="row form-group">
<label class="col-4">Topic</label>
<div class="col-8">
<input type="text" class="form-control mqtt-config" name="mqttPublishTopic" value="${config.mqttPublishTopic}"/>
</div>
</div>
</div>
<div class="col-md-3">
<div class="row form-group">
<label class="col-4">Username</label>
<div class="col-8">
<input type="text" class="form-control mqtt-config" name="mqttUser" value="${config.mqttUser}"/>
</div>
</div>
<div class="row form-group">
<label class="col-4">Password</label>
<div class="col-8">
<input type="password" class="form-control mqtt-config" name="mqttPassword" value="${config.mqttPassword}"/>
</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>
</main>
<script>
$('#mqttEnable').on('change', function() {
var inputs = $('.mqtt-config');
inputs.prop('disabled', !$(this).is(':checked'));
});
$(function() {
$('#mqttEnable').trigger('change');
});
</script>
</body>
</html>

View File

@@ -1,99 +0,0 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AMS reader - System configuration</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" type="text/css" href="boot.css"/>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css"/>
</head>
<body class="bg-light">
<main role="main" class="container">
<header class="navbar navbar-expand navbar-dark flex-column flex-md-row bg-purple rounded mt-2 mb-4" style="background-color: var(--purple);">
<a href="/" class=""><h6 class="navbar-brand">AMS reader <small>${version}</small></h6></a>
<div class="navbar-nav-scroll">
<ul class="navbar-nav bd-navbar-nav flex-row">
<li class="nav-item">
<a class="nav-link" href="/config-meter">Meter</a>
</li>
<li class="nav-item">
<a class="nav-link " href="/config-wifi">WiFi</a>
</li>
<li class="nav-item">
<a class="nav-link " href="/config-mqtt">MQTT</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config-web">Web</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/config-system">System</a>
</li>
</ul>
</div>
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
<li class="nav-item">
<a class="nav-link p-2" href="https://github.com/gskjold/AmsToMqttBridge" target="_blank" rel="noopener" aria-label="GitHub">
<svg class="d-inline-block align-text-top" style="width: 2rem; height: 2rem;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 499.36" focusable="false"><title>GitHub</title><path d="M256 0C114.64 0 0 114.61 0 256c0 113.09 73.34 209 175.08 242.9 12.8 2.35 17.47-5.56 17.47-12.34 0-6.08-.22-22.18-.35-43.54-71.2 15.49-86.2-34.34-86.2-34.34-11.64-29.57-28.42-37.45-28.42-37.45-23.27-15.84 1.73-15.55 1.73-15.55 25.69 1.81 39.21 26.38 39.21 26.38 22.84 39.12 59.92 27.82 74.5 21.27 2.33-16.54 8.94-27.82 16.25-34.22-56.84-6.43-116.6-28.43-116.6-126.49 0-27.95 10-50.8 26.35-68.69-2.63-6.48-11.42-32.5 2.51-67.75 0 0 21.49-6.88 70.4 26.24a242.65 242.65 0 0 1 128.18 0c48.87-33.13 70.33-26.24 70.33-26.24 14 35.25 5.18 61.27 2.55 67.75 16.41 17.9 26.31 40.75 26.31 68.69 0 98.35-59.85 120-116.88 126.32 9.19 7.9 17.38 23.53 17.38 47.41 0 34.22-.31 61.83-.31 70.23 0 6.85 4.61 14.81 17.6 12.31C438.72 464.97 512 369.08 512 256.02 512 114.62 397.37 0 256 0z" fill="currentColor" fill-rule="evenodd"></path></svg>
</a>
</li>
</ul>
</header>
<form method="post" action="/save">
<input type="hidden" name="sysConfig" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<div class="row">
<div class="col-md-3">
<label><input type="checkbox" name="debugTelnet" value="true" ${config.debugTelnet}/> Telnet debugger</label>
</div>
<div class="col-md-3">
<label><input type="checkbox" name="debugSerial" value="true" ${config.debugSerial}/> Serial debugger</label>
</div>
<div class="col-md-5">
<div class="row form-group">
<label class="col-6">Debug level</label>
<div class="col-6">
<select class="form-control" name="debugLevel">
<option value="2" ${config.debugLevel2}>Debug</option>
<option value="3" ${config.debugLevel3}>Info</option>
<option value="4" ${config.debugLevel4}>Warning</option>
<option value="5" ${config.debugLevel5}>Error</option>
</select>
</div>
</div>
</div>
<div class="col-md-1 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</div>
</form>
<form method="post" enctype="multipart/form-data">
<div class="my-3 p-3 bg-white rounded shadow">
<div class="alert alert-warning">!!WARNING!!<br/>Do not use this unless you know what you are doing. Uploading the wrong image could cause your device to stop working. Use with extreme caution!</div>
<div class="row">
<div class="col-md-4">
<div class="row form-group">
<label class="col-4">Firmware</label>
<div class="col-8">
<input type="file" name="file"/>
</div>
</div>
</div>
<div class="col-md-8 text-right">
<button class="btn btn-primary">Upload</button>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
</div>
</div>
</form>
</main>
</body>
</html>

View File

@@ -1,97 +0,0 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AMS reader - Meter configuration</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" type="text/css" href="boot.css"/>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
</head>
<body class="bg-light">
<main role="main" class="container">
<header class="navbar navbar-expand navbar-dark flex-column flex-md-row bg-purple rounded mt-2 mb-4" style="background-color: var(--purple);">
<a href="/" class=""><h6 class="navbar-brand">AMS reader <small>${version}</small></h6></a>
<div class="navbar-nav-scroll">
<ul class="navbar-nav bd-navbar-nav flex-row">
<li class="nav-item">
<a class="nav-link" href="/config-meter">Meter</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config-wifi">WiFi</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config-mqtt">MQTT</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/config-web">Web</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config-system">System</a>
</li>
</ul>
</div>
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
<li class="nav-item">
<a class="nav-link p-2" href="https://github.com/gskjold/AmsToMqttBridge" target="_blank" rel="noopener" aria-label="GitHub">
<svg class="d-inline-block align-text-top" style="width: 2rem; height: 2rem;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 499.36" focusable="false"><title>GitHub</title><path d="M256 0C114.64 0 0 114.61 0 256c0 113.09 73.34 209 175.08 242.9 12.8 2.35 17.47-5.56 17.47-12.34 0-6.08-.22-22.18-.35-43.54-71.2 15.49-86.2-34.34-86.2-34.34-11.64-29.57-28.42-37.45-28.42-37.45-23.27-15.84 1.73-15.55 1.73-15.55 25.69 1.81 39.21 26.38 39.21 26.38 22.84 39.12 59.92 27.82 74.5 21.27 2.33-16.54 8.94-27.82 16.25-34.22-56.84-6.43-116.6-28.43-116.6-126.49 0-27.95 10-50.8 26.35-68.69-2.63-6.48-11.42-32.5 2.51-67.75 0 0 21.49-6.88 70.4 26.24a242.65 242.65 0 0 1 128.18 0c48.87-33.13 70.33-26.24 70.33-26.24 14 35.25 5.18 61.27 2.55 67.75 16.41 17.9 26.31 40.75 26.31 68.69 0 98.35-59.85 120-116.88 126.32 9.19 7.9 17.38 23.53 17.38 47.41 0 34.22-.31 61.83-.31 70.23 0 6.85 4.61 14.81 17.6 12.31C438.72 464.97 512 369.08 512 256.02 512 114.62 397.37 0 256 0z" fill="currentColor" fill-rule="evenodd"></path></svg>
</a>
</li>
</ul>
</header>
<form method="post" action="/save">
<input type="hidden" name="authConfig" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<div class="row">
<div class="col-md-4">
<div class="row form-group">
<label class="col-4">Security</label>
<div class="col-8">
<select id="authSecurity" class="form-control" name="authSecurity">
<option value="0" ${config.authSecurity0}>None</option>
<option value="1" ${config.authSecurity1}>Only configuration</option>
<option value="2" ${config.authSecurity2}>Everything</option>
</select>
</div>
</div>
</div>
<div class="col-md-4">
<div class="row form-group">
<label class="col-4">Username</label>
<div class="col-8">
<input type="text" class="form-control auth-config" name="authUser" value="${config.authUser}"/>
</div>
</div>
</div>
<div class="col-md-4">
<div class="row form-group">
<label class="col-4">Password</label>
<div class="col-8">
<input type="password" class="form-control auth-config" name="authPassword" value="${config.authPassword}"/>
</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>
</main>
<script>
$('#authSecurity').on('change', function() {
var inputs = $('.auth-config');
inputs.prop('disabled', $(this).val() == 0);
});
$(function() {
$('#authSecurity').trigger('change');
});
</script>
</body>
</html>

View File

@@ -1,132 +0,0 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AMS reader - WiFi configuration</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" type="text/css" href="boot.css"/>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
</head>
<body class="bg-light">
<main role="main" class="container">
<header class="navbar navbar-expand navbar-dark flex-column flex-md-row bg-purple rounded mt-2 mb-4" style="background-color: var(--purple);">
<a href="/" class=""><h6 class="navbar-brand">AMS reader <small>${version}</small></h6></a>
<div class="navbar-nav-scroll">
<ul class="navbar-nav bd-navbar-nav flex-row">
<li class="nav-item">
<a class="nav-link" href="/config-meter">Meter</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/config-wifi">WiFi</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config-mqtt">MQTT</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config-web">Web</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config-system">System</a>
</li>
</ul>
</div>
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
<li class="nav-item">
<a class="nav-link p-2" href="https://github.com/gskjold/AmsToMqttBridge" target="_blank" rel="noopener" aria-label="GitHub">
<svg class="d-inline-block align-text-top" style="width: 2rem; height: 2rem;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 499.36" focusable="false"><title>GitHub</title><path d="M256 0C114.64 0 0 114.61 0 256c0 113.09 73.34 209 175.08 242.9 12.8 2.35 17.47-5.56 17.47-12.34 0-6.08-.22-22.18-.35-43.54-71.2 15.49-86.2-34.34-86.2-34.34-11.64-29.57-28.42-37.45-28.42-37.45-23.27-15.84 1.73-15.55 1.73-15.55 25.69 1.81 39.21 26.38 39.21 26.38 22.84 39.12 59.92 27.82 74.5 21.27 2.33-16.54 8.94-27.82 16.25-34.22-56.84-6.43-116.6-28.43-116.6-126.49 0-27.95 10-50.8 26.35-68.69-2.63-6.48-11.42-32.5 2.51-67.75 0 0 21.49-6.88 70.4 26.24a242.65 242.65 0 0 1 128.18 0c48.87-33.13 70.33-26.24 70.33-26.24 14 35.25 5.18 61.27 2.55 67.75 16.41 17.9 26.31 40.75 26.31 68.69 0 98.35-59.85 120-116.88 126.32 9.19 7.9 17.38 23.53 17.38 47.41 0 34.22-.31 61.83-.31 70.23 0 6.85 4.61 14.81 17.6 12.31C438.72 464.97 512 369.08 512 256.02 512 114.62 397.37 0 256 0z" fill="currentColor" fill-rule="evenodd"></path></svg>
</a>
</li>
</ul>
</header>
<form method="post" action="/save">
<input type="hidden" name="wifiConfig" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<div class="row">
<div class="col-md-4">
<div class="row form-group">
<label class="col-3">SSID</label>
<div class="col-9">
<input type="text" class="form-control" name="wifiSsid" value="${config.wifiSsid}"/>
</div>
</div>
<div class="row form-group">
<label class="col-3">Password</label>
<div class="col-9">
<input type="password" class="form-control" name="wifiPassword" value="${config.wifiPassword}"/>
</div>
</div>
<div class="row form-group">
<label class="col-3">Hostname</label>
<div class="col-9">
<input type="text" class="form-control" name="wifiHostname" value="${config.wifiHostname}"/>
</div>
</div>
</div>
<div class="col-md-4">
<div class="row form-group">
<label class="col-6">IP configuration</label>
<div class="col-6">
<select id="wifiIpType" class="form-control" name="wifiIpType">
<option value="0" ${config.wifiIpType0}>DHCP</option>
<option value="1" ${config.wifiIpType1}>Static</option>
</select>
</div>
</div>
<div class="row form-group">
<label class="col-6">IP</label>
<div class="col-6">
<input type="text" class="form-control wifiip-config" name="wifiIp" value="${config.wifiIp}"/>
</div>
</div>
<div class="row form-group">
<label class="col-6">Subnet</label>
<div class="col-6">
<input type="text" class="form-control wifiip-config" name="wifiSubnet" value="${config.wifiSubnet}"/>
</div>
</div>
</div>
<div class="col-md-4">
<div class="row form-group">
<label class="col-6">Gateway</label>
<div class="col-6">
<input type="text" class="form-control wifiip-config" name="wifiGw" value="${config.wifiGw}"/>
</div>
</div>
<div class="row form-group">
<label class="col-6">Primary DNS</label>
<div class="col-6">
<input type="text" class="form-control wifiip-config" name="wifiDns1" value="${config.wifiDns1}"/>
</div>
</div>
<div class="row form-group">
<label class="col-6">Secondary DNS</label>
<div class="col-6">
<input type="text" class="form-control wifiip-config" name="wifiDns2" value="${config.wifiDns2}"/>
</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>
</main>
<script>
$('#wifiIpType').on('change', function() {
var inputs = $('.wifiip-config');
inputs.prop('disabled', $(this).val() != 1);
});
$(function() {
$('#wifiIpType').trigger('change');
});
</script>
</body>
</html>

34
web/data.json Normal file
View File

@@ -0,0 +1,34 @@
{
"im" : %d,
"om" : %d,
"mf" : %d,
"i" : %d,
"e" : %d,
"ri" : %d,
"re" : %d,
"ic" : %.2f,
"ec" : %.2f,
"ric" : %.2f,
"rec" : %.2f,
"u1" : %.2f,
"u2" : %.2f,
"u3" : %.2f,
"i1" : %.2f,
"i2" : %.2f,
"i3" : %.2f,
"f" : %.2f,
"f1" : %.2f,
"f2" : %.2f,
"f3" : %.2f,
"v" : %.3f,
"r" : %d,
"t" : %.2f,
"u" : %lu,
"m" : %lu,
"em" : %d,
"hm" : %d,
"wm" : %d,
"mm" : %d,
"me" : %d,
"p" : %s
}

26
web/dayplot.json Normal file
View File

@@ -0,0 +1,26 @@
{
"h00" : %.2f,
"h01" : %.2f,
"h02" : %.2f,
"h03" : %.2f,
"h04" : %.2f,
"h05" : %.2f,
"h06" : %.2f,
"h07" : %.2f,
"h08" : %.2f,
"h09" : %.2f,
"h10" : %.2f,
"h11" : %.2f,
"h12" : %.2f,
"h13" : %.2f,
"h14" : %.2f,
"h15" : %.2f,
"h16" : %.2f,
"h17" : %.2f,
"h18" : %.2f,
"h19" : %.2f,
"h20" : %.2f,
"h21" : %.2f,
"h22" : %.2f,
"h23" : %.2f
}

37
web/debugging.html Normal file
View File

@@ -0,0 +1,37 @@
<div class="alert alert-warning">!!NOTE!!<br/>Telnet debugging is not considered safe and should be switched off when not in use</div>
<form method="post" action="/save">
<input type="hidden" name="debugConfig" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>Debugging</h6>
<div class="row">
<div class="col-xl-2 col-md-3">
<label><input type="checkbox" name="debugTelnet" value="true" ${config.debugTelnet}/> Telnet debugger</label>
</div>
<div class="col-xl-2 col-md-3">
<label><input type="checkbox" name="debugSerial" value="true" ${config.debugSerial}/> Serial debugger</label>
</div>
<div class="col-xl-3 col-md-4">
<div class="row form-group">
<label class="col-6">Debug level</label>
<div class="col-6">
<select class="form-control form-control-sm" name="debugLevel">
<option value="2" ${config.debugLevel2}>Debug</option>
<option value="3" ${config.debugLevel3}>Info</option>
<option value="4" ${config.debugLevel4}>Warning</option>
<option value="5" ${config.debugLevel5}>Error</option>
</select>
</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

14
web/delete.html Normal file
View File

@@ -0,0 +1,14 @@
<form method="post">
<div class="my-3 p-3 bg-white rounded shadow">
<div class="alert alert-warning">Are you sure you want to delete this file?</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="javascript:history.back();" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-danger">Delete</button>
</div>
</div>
</form>

48
web/domoticz.html Normal file
View File

@@ -0,0 +1,48 @@
<form method="post" action="/save">
<input type="hidden" name="dc" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<div class="d-flex flex-row flex-wrap">
<div class="m-2 input-group input-group-sm" style="width: 200px;">
<div class="input-group-prepend">
<span class="input-group-text">Electricity IDX</span>
</div>
<input type="number" class="form-control" name="elidx" value="{elidx}" min="0" max="65535"/>
</div>
<div class="m-2 input-group input-group-sm" style="width: 240px;">
<div class="input-group-prepend">
<span class="input-group-text">Current (3 Phase) IDX</span>
</div>
<input type="number" class="form-control" name="cl1idx" value="{cl1idx}" min="0" max="65535"/>
</div>
</div>
<div class="d-flex flex-row flex-wrap">
<div class="m-2 input-group input-group-sm" style="width: 200px;">
<div class="input-group-prepend">
<span class="input-group-text">Voltage L1 IDX</span>
</div>
<input type="number" class="form-control" name="vl1idx" value="{vl1idx}" min="0" max="65535"/>
</div>
<div class="m-2 input-group input-group-sm" style="width: 200px;">
<div class="input-group-prepend">
<span class="input-group-text">Voltage L1 IDX</span>
</div>
<input type="number" class="form-control" name="vl2idx" value="{vl2idx}" min="0" max="65535"/>
</div>
<div class="m-2 input-group input-group-sm" style="width: 200px;">
<div class="input-group-prepend">
<span class="input-group-text">Voltage L1 IDX</span>
</div>
<input type="number" class="form-control" name="vl3idx" value="{vl3idx}" min="0" max="65535"/>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

6
web/domoticz.json Normal file
View File

@@ -0,0 +1,6 @@
{
"command" : "udevice",
"idx" : %d,
"nvalue" : 0,
"svalue" : "%s"
}

39
web/energyprice.json Normal file
View File

@@ -0,0 +1,39 @@
{
"currency" : "%s",
"00" : %s,
"01" : %s,
"02" : %s,
"03" : %s,
"04" : %s,
"05" : %s,
"06" : %s,
"07" : %s,
"08" : %s,
"09" : %s,
"10" : %s,
"11" : %s,
"12" : %s,
"13" : %s,
"14" : %s,
"15" : %s,
"16" : %s,
"17" : %s,
"18" : %s,
"19" : %s,
"20" : %s,
"21" : %s,
"22" : %s,
"23" : %s,
"24" : %s,
"25" : %s,
"26" : %s,
"27" : %s,
"28" : %s,
"29" : %s,
"30" : %s,
"31" : %s,
"32" : %s,
"33" : %s,
"34" : %s,
"35" : %s
}

72
web/entsoe.html Normal file
View File

@@ -0,0 +1,72 @@
<form method="post" action="/save">
<input type="hidden" name="ec" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>ENTSO-E API</h6>
<div class="row">
<div class="col-xl-4 col-lg-6 col-md-8">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Token</span>
</div>
<input type="text" name="et" class="form-control" value="{et}"/>
</div>
</div>
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Region</span>
</div>
<select name="ea" class="form-control">
<optgroup label="Norway">
<option value="10YNO-1--------2" {eaNo1}>NO1</option>
<option value="10YNO-2--------T" {eaNo2}>NO2</option>
<option value="10YNO-3--------J" {eaNo3}>NO3</option>
<option value="10YNO-4--------9" {eaNo4}>NO4</option>
<option value="10Y1001A1001A48H" {eaNo5}>NO5</option>
</optgroup>
<optgroup label="Sweden">
<option value="10Y1001A1001A44P" {eaSe1}>SE1</option>
<option value="10Y1001A1001A45N" {eaSe2}>SE2</option>
<option value="10Y1001A1001A46L" {eaSe3}>SE3</option>
<option value="10Y1001A1001A47J" {eaSe4}>SE4</option>
</optgroup>
<optgroup label="Denmark">
<option value="10YDK-1--------W" {eaDk1}>DK1</option>
<option value="10YDK-2--------M" {eaDk2}>DK2</option>
</optgroup>
</select>
</div>
</div>
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Currency</span>
</div>
<select name="ecu" class="form-control">
<option value="NOK" {ecNOK}>NOK</option>
<option value="SEK" {ecSEK}>SEK</option>
<option value="DKK" {ecDKK}>DKK</option>
<option value="EUR" {ecEUR}>EUR</option>
</select>
</div>
</div>
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Multiplier</span>
</div>
<input name="em" type="number" min="0.001" max="1000" step="0.001" class="form-control" value="{em}"/>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

34
web/firmware.html Normal file
View File

@@ -0,0 +1,34 @@
<div class="alert alert-danger">
WARNING: Units powered over M-bus must be connected to an external power supply during firmware upload. Failure to do so may cause power-down during upload resulting in non-functioning unit.
</div>
<div class="alert alert-warning">
Your board is using {chipset} chipset. Only upload firmware designed for this chipset. Failure to do so may result in non-functioning unit.
<span id="fwDownload" style="display: none;"><br/>Download latest firmware file <a id="fwLink" href="#" data-chipset="{chipset}">here</a></span>
</div>
<form method="post" enctype="multipart/form-data" class="upload-form">
<div class="my-3 p-3 bg-white rounded shadow">
<div class="row">
<div class="col-lg-6">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">Upload</span>
</div>
<div class="custom-file">
<input name="file" type="file" class="custom-file-input" id="fileUploadField">
<label class="custom-file-label" for="fileUploadField">Choose file</label>
</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="javascript:history.back();" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Upload</button>
</div>
</div>
</form>

21
web/foot.html Normal file
View File

@@ -0,0 +1,21 @@
<div id="newVersion" class="alert alert-info d-none">New version <span id="newVersionTag"></span>!
<a id="newVersionUrl" href="#" target="_blank">view</a>
<span class="d-none ssl-capable"> or <a href="javascript:upgrade();">upgrade</a></span>
</div>
</main>
<div id="loading-indicator" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: #dddddd99; z-index: 999999; padding-top: 20%; display: none;" class="text-center">
<div class="spinner-border text-primary" role="status" style="width: 5rem; height: 5rem;">
<span class="sr-only">Loading...</span>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
<script src="https://www.gstatic.com/charts/loader.js"></script>
<script src="application-${version}.js"></script>
</body>
</html>

View File

@@ -1,275 +0,0 @@
/*
* AshAlom Gauge Meter. Version 2.0.0
* Copyright AshAlom.com All rights reserved.
* https://github.com/AshAlom/GaugeMeter <- Deleted!
* https://github.com/githubsrinath/GaugeMeter <- Backup original.
*
* Original created by Dr Ash Alom
*
* This is a bug fixed and modified version of the AshAlom Gauge Meter.
* Copyright 2018 Michael Wolf (Mictronics)
* https://github.com/mictronics/GaugeMeter
*
*/
!function ($) {
$.fn.gaugeMeter = function (t) {
var defaults = $.extend({
id: "",
percent: 0,
used: null,
min: null,
total: null,
size: 100,
prepend: "",
append: "",
theme: "Red-Gold-Green",
color: "",
back: "RGBa(0,0,0,.06)",
width: 3,
style: "Full",
stripe: "0",
animationstep: 1,
animate_gauge_colors: false,
animate_text_colors: false,
label: "",
label_color: "Black",
text: "",
text_size: 0.22,
fill: "",
showvalue: false
}, t);
return this.each(function () {
function getThemeColor(e) {
var t = "#2C94E0";
return e || (e = 1e-14),
"Red-Gold-Green" === option.theme && (e > 0 && (t = "#d90000"), e > 10 && (t = "#e32100"), e > 20 && (t = "#f35100"), e > 30 && (t = "#ff8700"), e > 40 && (t = "#ffb800"), e > 50 && (t = "#ffd900"), e > 60 && (t = "#dcd800"), e > 70 && (t = "#a6d900"), e > 80 && (t = "#69d900"), e > 90 && (t = "#32d900")),
"Green-Gold-Red" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#69d900"), e > 20 && (t = "#a6d900"), e > 30 && (t = "#dcd800"), e > 40 && (t = "#ffd900"), e > 50 && (t = "#ffb800"), e > 60 && (t = "#ff8700"), e > 70 && (t = "#f35100"), e > 80 && (t = "#e32100"), e > 90 && (t = "#d90000")),
"Green-Red" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#41c900"), e > 20 && (t = "#56b300"), e > 30 && (t = "#6f9900"), e > 40 && (t = "#8a7b00"), e > 50 && (t = "#a75e00"), e > 60 && (t = "#c24000"), e > 70 && (t = "#db2600"), e > 80 && (t = "#f01000"), e > 90 && (t = "#ff0000")),
"Red-Green" === option.theme && (e > 0 && (t = "#ff0000"), e > 10 && (t = "#f01000"), e > 20 && (t = "#db2600"), e > 30 && (t = "#c24000"), e > 40 && (t = "#a75e00"), e > 50 && (t = "#8a7b00"), e > 60 && (t = "#6f9900"), e > 70 && (t = "#56b300"), e > 80 && (t = "#41c900"), e > 90 && (t = "#32d900")),
"DarkBlue-LightBlue" === option.theme && (e > 0 && (t = "#2c94e0"), e > 10 && (t = "#2b96e1"), e > 20 && (t = "#2b99e4"), e > 30 && (t = "#2a9ce7"), e > 40 && (t = "#28a0e9"), e > 50 && (t = "#26a4ed"), e > 60 && (t = "#25a8f0"), e > 70 && (t = "#24acf3"), e > 80 && (t = "#23aff5"), e > 90 && (t = "#21b2f7")),
"LightBlue-DarkBlue" === option.theme && (e > 0 && (t = "#21b2f7"), e > 10 && (t = "#23aff5"), e > 20 && (t = "#24acf3"), e > 30 && (t = "#25a8f0"), e > 40 && (t = "#26a4ed"), e > 50 && (t = "#28a0e9"), e > 60 && (t = "#2a9ce7"), e > 70 && (t = "#2b99e4"), e > 80 && (t = "#2b96e1"), e > 90 && (t = "#2c94e0")),
"DarkRed-LightRed" === option.theme && (e > 0 && (t = "#d90000"), e > 10 && (t = "#dc0000"), e > 20 && (t = "#e00000"), e > 30 && (t = "#e40000"), e > 40 && (t = "#ea0000"), e > 50 && (t = "#ee0000"), e > 60 && (t = "#f30000"), e > 70 && (t = "#f90000"), e > 80 && (t = "#fc0000"), e > 90 && (t = "#ff0000")),
"LightRed-DarkRed" === option.theme && (e > 0 && (t = "#ff0000"), e > 10 && (t = "#fc0000"), e > 20 && (t = "#f90000"), e > 30 && (t = "#f30000"), e > 40 && (t = "#ee0000"), e > 50 && (t = "#ea0000"), e > 60 && (t = "#e40000"), e > 70 && (t = "#e00000"), e > 80 && (t = "#dc0000"), e > 90 && (t = "#d90000")),
"DarkGreen-LightGreen" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#33db00"), e > 20 && (t = "#34df00"), e > 30 && (t = "#34e200"), e > 40 && (t = "#36e700"), e > 50 && (t = "#37ec00"), e > 60 && (t = "#38f100"), e > 70 && (t = "#38f600"), e > 80 && (t = "#39f900"), e > 90 && (t = "#3afc00")),
"LightGreen-DarkGreen" === option.theme && (e > 0 && (t = "#3afc00"), e > 10 && (t = "#39f900"), e > 20 && (t = "#38f600"), e > 30 && (t = "#38f100"), e > 40 && (t = "#37ec00"), e > 50 && (t = "#36e700"), e > 60 && (t = "#34e200"), e > 70 && (t = "#34df00"), e > 80 && (t = "#33db00"), e > 90 && (t = "#32d900")),
"DarkGold-LightGold" === option.theme && (e > 0 && (t = "#ffb800"), e > 10 && (t = "#ffba00"), e > 20 && (t = "#ffbd00"), e > 30 && (t = "#ffc200"), e > 40 && (t = "#ffc600"), e > 50 && (t = "#ffcb00"), e > 60 && (t = "#ffcf00"), e > 70 && (t = "#ffd400"), e > 80 && (t = "#ffd600"), e > 90 && (t = "#ffd900")),
"LightGold-DarkGold" === option.theme && (e > 0 && (t = "#ffd900"), e > 10 && (t = "#ffd600"), e > 20 && (t = "#ffd400"), e > 30 && (t = "#ffcf00"), e > 40 && (t = "#ffcb00"), e > 50 && (t = "#ffc600"), e > 60 && (t = "#ffc200"), e > 70 && (t = "#ffbd00"), e > 80 && (t = "#ffba00"), e > 90 && (t = "#ffb800")),
"White" === option.theme && (t = "#fff"),
"Black" === option.theme && (t = "#000"),
t;
}
/* The label below gauge. */
function createLabel(t, a) {
if(t.children("b").length === 0){
$("<b></b>").appendTo(t).html(option.label).css({
"line-height": option.size + 5 * a + "px",
color: option.label_color
});
}
}
/* Prepend and append text, the gauge text or percentage value. */
function createSpanTag(t) {
var fgcolor = "";
if (option.animate_text_colors === true){
fgcolor = option.fgcolor;
}
var child = t.children("span");
if(child.length !== 0){
child.html(r).css({color: fgcolor});
return;
}
if(option.text_size <= 0.0 || Number.isNaN(option.text_size)){
option.text_size = 0.22;
}
if(option.text_size > 0.5){
option.text_size = 0.5;
}
$("<span></span>").appendTo(t).html(r).css({
"line-height": option.size + "px",
"font-size": option.text_size * option.size + "px",
color: fgcolor
});
}
/* Get data attributes as options from div tag. Fall back to defaults when not exists. */
function getDataAttr(t) {
$.each(dataAttr, function (index, element) {
if(t.data(element) !== undefined && t.data(element) !== null){
option[element] = t.data(element);
} else {
option[element] = $(defaults).attr(element);
}
if(element === "fill"){
s = option[element];
}
if((element === "size" ||
element === "width" ||
element === "animationstep" ||
element === "stripe"
) && !Number.isInteger(option[element])){
option[element] = parseInt(option[element]);
}
if(element === "text_size"){
option[element] = parseFloat(option[element]);
}
});
}
/* Draws the gauge. */
function drawGauge(a) {
if(M < 0) M = 0;
if(M > 100) M = 100;
var lw = option.width < 1 || isNaN(option.width) ? option.size / 20 : option.width;
g.clearRect(0, 0, b.width, b.height);
g.beginPath();
g.arc(m, v, x, G, k, !1);
if(s){
g.fillStyle = option.fill;
g.fill();
}
g.lineWidth = lw;
g.strokeStyle = option.back;
option.stripe > parseInt(0) ? g.setLineDash([option.stripe], 1) : g.lineCap = "round";
g.stroke();
g.beginPath();
g.arc(m, v, x, -I, P * a - I, !1);
g.lineWidth = lw;
g.strokeStyle = option.fgcolor;
g.stroke();
c > M && (M += z, requestAnimationFrame(function(){
drawGauge(Math.min(M, c) / 100);
}, p));
}
$(this).attr("data-id", $(this).attr("id"));
var r,
dataAttr = ["percent",
"used",
"min",
"total",
"size",
"prepend",
"append",
"theme",
"color",
"back",
"width",
"style",
"stripe",
"animationstep",
"animate_gauge_colors",
"animate_text_colors",
"label",
"label_color",
"text",
"text_size",
"fill",
"showvalue"],
option = {},
c = 0,
p = $(this),
s = false;
p.addClass("gaugeMeter");
getDataAttr(p);
if(Number.isInteger(option.used) && Number.isInteger(option.total)){
var u = option.used;
var t = option.total;
if(Number.isInteger(option.min)) {
if(option.min < 0) {
t -= option.min;
u -= option.min;
}
}
c = u / (t / 100);
} else {
if(Number.isInteger(option.percent)){
c = option.percent;
} else {
c = parseInt(defaults.percent);
}
}
if(c < 0) c = 0;
if(c > 100) c = 100;
if( option.text !== "" && option.text !== null && option.text !== undefined){
if(option.append !== "" && option.append !== null && option.append !== undefined){
r = option.text + "<u>" + option.append + "</u>";
} else {
r = option.text;
}
if(option.prepend !== "" && option.prepend !== null && option.prepend !== undefined){
r = "<s>" + option.prepend + "</s>" + r;
}
} else {
if(defaults.showvalue === true || option.showvalue === true){
r = option.used;
} else {
r = c.toString();
}
if(option.prepend !== "" && option.prepend !== null && option.prepend !== undefined){
r = "<s>" + option.prepend + "</s>" + r;
}
if(option.append !== "" && option.append !== null && option.append !== undefined){
r = r + "<u>" + option.append + "</u>";
}
}
option.fgcolor = getThemeColor(c);
if(option.color !== "" && option.color !== null && option.color !== undefined){
option.fgcolor = option.color;
}
if(option.animate_gauge_colors === true){
option.fgcolor = getThemeColor(c);
}
createSpanTag(p);
if(option.style !== "" && option.style !== null && option.style !== undefined){
createLabel(p, option.size / 13);
}
$(this).width(option.size + "px");
var b = $("<canvas></canvas>").attr({width: option.size, height: option.size}).get(0),
g = b.getContext("2d"),
m = b.width / 2,
v = b.height / 2,
_ = 360 * option.percent,
x = (_ * (Math.PI / 180), b.width / 2.5),
k = 2.3 * Math.PI,
G = 0,
M = 0 === option.animationstep ? c : 0,
z = Math.max(option.animationstep, 0),
P = 2 * Math.PI,
I = Math.PI / 2,
R = option.style;
var child = $(this).children("canvas");
if(child.length !== 0){
/* Replace existing canvas when new percentage was written. */
child.replaceWith(b);
} else {
/* Initially create canvas. */
$(b).appendTo($(this));
}
if ("Semi" === R){
k = 2 * Math.PI;
G = 3.13;
P = 1 * Math.PI;
I = Math.PI / .996;
}
if ("Arch" === R){
k = 2.195 * Math.PI;
G = 1, G = 655.99999;
P = 1.4 * Math.PI;
I = Math.PI / .8335;
}
drawGauge(M / 100);
});
};
}
(jQuery);

6
web/github.svg Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 499.36" focusable="false">
<title>GitHub</title>
<path d="M256 0C114.64 0 0 114.61 0 256c0 113.09 73.34 209 175.08 242.9 12.8 2.35 17.47-5.56 17.47-12.34 0-6.08-.22-22.18-.35-43.54-71.2 15.49-86.2-34.34-86.2-34.34-11.64-29.57-28.42-37.45-28.42-37.45-23.27-15.84 1.73-15.55 1.73-15.55 25.69 1.81 39.21 26.38 39.21 26.38 22.84 39.12 59.92 27.82 74.5 21.27 2.33-16.54 8.94-27.82 16.25-34.22-56.84-6.43-116.6-28.43-116.6-126.49 0-27.95 10-50.8 26.35-68.69-2.63-6.48-11.42-32.5 2.51-67.75 0 0 21.49-6.88 70.4 26.24a242.65 242.65 0 0 1 128.18 0c48.87-33.13 70.33-26.24 70.33-26.24 14 35.25 5.18 61.27 2.55 67.75 16.41 17.9 26.31 40.75 26.31 68.69 0 98.35-59.85 120-116.88 126.32 9.19 7.9 17.38 23.53 17.38 47.41 0 34.22-.31 61.83-.31 70.23 0 6.85 4.61 14.81 17.6 12.31C438.72 464.97 512 369.08 512 256.02 512 114.62 397.37 0 256 0z" fill="#f8f9fa" fill-rule="evenodd"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

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