Compare commits

..

192 Commits

Author SHA1 Message Date
Gunnar Skjold
70f5b0f912 Change default MQTT buffer 2022-04-23 08:48:34 +02:00
Gunnar Skjold
d724a90085 Fixed analog read of voltage 2022-04-17 10:07:25 +02:00
Gunnar Skjold
1ab529785a Fixed loop error 2022-04-14 10:50:00 +02:00
Gunnar Skjold
3a81e62bbe Support for encrypted DSMR 2022-04-14 10:38:04 +02:00
Gunnar Skjold
4882916b5c Fixed comment 2022-04-14 08:19:07 +02:00
Gunnar Skjold
1f7e43256a Fixed detection of sagemcom 2022-04-14 08:18:51 +02:00
Gunnar Skjold
a0d3632fd7 Fixed clearing of prices on MQTT 2022-04-04 07:31:08 +02:00
Gunnar Skjold
b25564a89f Clear all unknown prices instead of just last 2022-04-03 13:27:21 +02:00
Gunnar Skjold
191d9fa562 Added month max to mqtt 2022-04-01 20:39:22 +02:00
Gunnar Skjold
e009b4e54e Fixed price update every hour 2022-04-01 07:58:58 +02:00
Gunnar Skjold
811625b706 Adaptations for ESP8266 for last commit 2022-03-30 19:09:50 +02:00
Gunnar Skjold
15fa452c7c Fixed ENTSO-E for CEST fetch 2022-03-30 19:06:48 +02:00
Gunnar Skjold
e725c01597 Updated build for esp32 2022-03-30 17:38:08 +02:00
Gunnar Skjold
35b884713c Merge branch 'agpl' 2022-03-30 17:09:54 +02:00
Gunnar Skjold
d5fc9f6fc6 Remove temp screenshot 2022-03-30 17:05:29 +02:00
Gunnar Skjold
19c8396b1c Updated screenshot 2022-03-30 17:04:36 +02:00
Gunnar Skjold
f4622a068b Merge pull request #256 from gskjold/dev-v2.1.0
Dev v2.1.0
2022-03-30 17:00:55 +02:00
Gunnar Skjold
9922862299 Fixed -127 value on last hour of day when we enter CEST 2022-03-26 17:47:33 +01:00
Gunnar Skjold
e7fc504975 Fixed prices that are not cleared after midnight? 2022-03-26 09:45:35 +01:00
Gunnar Skjold
6cdb943763 Fixed decrypt on esp32 without auth key 2022-03-26 09:31:49 +01:00
Gunnar Skjold
3e937a4e5d Updated CI 2022-03-26 09:26:12 +01:00
Gunnar Skjold
fcd3e3ca25 Fixed firmware download for esp32-s2 2022-03-26 09:21:57 +01:00
Gunnar Skjold
772004c0a2 Added zip files to release resources, combined with flash script 2022-03-26 09:19:03 +01:00
Gunnar Skjold
c3e5ce344c Fixed S2 build 2022-03-25 21:16:18 +01:00
Gunnar Skjold
b11b1d5e37 Default meter config to 0 2022-03-25 20:37:34 +01:00
Gunnar Skjold
b5d34990df Fixed typ-o 2022-03-24 08:46:35 +01:00
Gunnar Skjold
a287d310ac Added ESP32-S2 build 2022-03-24 08:38:29 +01:00
Gunnar Skjold
afe6190c46 Merge pull request #251 from kng/dev-ha
ha discvovery, handle unset values
2022-03-23 07:34:03 +01:00
Daniel Ekman
2125945728 ha discvovery, handle unset values 2022-03-22 11:24:38 +01:00
Gunnar Skjold
fb60127cf7 Removed test code 2022-03-20 20:17:19 +01:00
Gunnar Skjold
a07c55889f Build fix 2022-03-20 20:11:29 +01:00
Gunnar Skjold
a675f56f9c Restart after uploading config file, then restore, then reboot again 2022-03-20 20:02:51 +01:00
Gunnar Skjold
0590ec375d Delete firmware file if exists after factory reset 2022-03-18 19:35:58 +01:00
Gunnar Skjold
48bd352619 Remove stat_cla property from HA discovery message if null 2022-03-18 19:19:37 +01:00
Gunnar Skjold
40016f314e Multipliers for HAN data 2022-03-18 19:16:03 +01:00
Gunnar Skjold
b9234f6f64 Fixed HA MQTT corruption 2022-03-17 20:36:21 +01:00
Gunnar Skjold
beafcd300b Added realtime day use to MQTT 2022-03-17 18:51:13 +01:00
Gunnar Skjold
a92524eaf8 Some adjustments for real time calculation 2022-03-17 18:47:57 +01:00
Gunnar Skjold
a60dd8a60a Added separate buffer for HAN data 2022-03-17 18:14:31 +01:00
Gunnar Skjold
53296ccf42 Fixed update of meter state with list type 4 2022-03-16 12:46:59 +01:00
Gunnar Skjold
42a627e32e Fixed download/upload config 2022-03-12 20:38:51 +01:00
Gunnar Skjold
c38034c69b Merge branch 'master' into dev-v2.1.0 2022-03-12 20:06:30 +01:00
Gunnar Skjold
0ec1a8ece3 Experimenting with fw upgrade for esp8266 2022-03-12 11:16:37 +01:00
Gunnar Skjold
45275c271e Changed to "correct" bootstrap js. Seems we have a mix, but ok 2022-03-12 11:12:49 +01:00
Gunnar Skjold
116761b46e Fixed overflow issue 2022-03-12 09:19:01 +01:00
Gunnar Skjold
a2c13fd0da Changed cursor for watt display 2022-03-12 09:01:08 +01:00
Gunnar Skjold
b727f84c72 Possibility to show watt instead of kw 2022-03-12 08:56:09 +01:00
Gunnar Skjold
3706625760 Changed colors for amp graph 2022-03-12 08:46:18 +01:00
Gunnar Skjold
0e1d2211ec Added SRI to js/css resources 2022-03-12 08:40:34 +01:00
Gunnar Skjold
785bec0269 Added export to graph 2022-03-12 08:26:24 +01:00
Gunnar Skjold
2a4a199ae6 Updated python version for release action 2022-03-12 08:26:08 +01:00
Gunnar Skjold
b6b9db5431 Merge branch 'master' into dev-v2.1.0 2022-03-11 19:48:20 +01:00
Gunnar Skjold
55bdd4437a Fixed drawing amp graph when all phases export 2022-03-11 19:44:51 +01:00
Gunnar Skjold
3948fff184 Fixed calculation of highest hour 2022-03-11 19:42:44 +01:00
Gunnar Skjold
c4b099b752 Added profile for Pow-K+ 2022-03-11 19:28:28 +01:00
Gunnar Skjold
5cfcc015f4 Fixed hostname for esp32 2022-03-11 19:21:27 +01:00
Gunnar Skjold
724768afc4 Fixed MQTT problem 2022-03-11 19:16:08 +01:00
Gunnar Skjold
efc20f1da9 Merge branch 'master' into dev-v2.1.0 2022-03-11 18:58:41 +01:00
Gunnar Skjold
b071344871 Fixed reading of numbers. Added support for newer Kaifa payload 2022-03-11 18:55:01 +01:00
Gunnar Skjold
e0e98f0ec1 Logging 2022-02-28 10:45:09 +01:00
Gunnar Skjold
53d34852f1 Fixed labels for domoticz 2022-02-28 08:31:53 +01:00
Gunnar Skjold
c26073b2a3 Some adjustments to make config file download work again 2022-02-26 20:47:34 +01:00
Gunnar Skjold
b12a632618 Some memory optimization 2022-02-26 16:03:46 +01:00
Gunnar Skjold
420be1b943 Some memory optimization and configfile download fix 2022-02-26 14:57:12 +01:00
Gunnar Skjold
142d280805 Consolidated two HA discovery JSON to one 2022-02-26 14:02:26 +01:00
Gunnar Skjold
196b79e4c8 Fixed corrupted payload for HA 2022-02-26 13:57:01 +01:00
Gunnar Skjold
2ef1700cfe Added static HA content to progmem 2022-02-22 21:33:50 +01:00
Gunnar Skjold
cbbb52d32a Some cleanup and buffer sharing 2022-02-22 21:05:34 +01:00
Gunnar Skjold
6bded12ec5 Increased buffer size for HA payload 2022-02-20 21:20:10 +01:00
Gunnar Skjold
5ae56f265e Some final changes before releasing for test 2022-02-20 20:10:08 +01:00
Gunnar Skjold
705b3e34d1 Swedish kamstrup frame with wh instead of wh/10 2022-02-20 19:41:29 +01:00
Gunnar Skjold
fcb4ccb462 Merge pull request #240 from mikkle/mqtt_mem_free
publish ESP free mem in mqtt publishSystem
2022-02-20 19:30:20 +01:00
Mikkel Troest
e0cd14e904 publish ESP free mem in mqtt publishSystem 2022-02-20 15:39:52 +01:00
Gunnar Skjold
bb6e8838b4 Adaptation to 2.1 contract 2022-02-20 11:22:14 +01:00
Gunnar Skjold
5325f8f845 Merge branch 'pr-172' into dev-v2.1.0 2022-02-20 11:17:33 +01:00
Gunnar Skjold
113b8bf820 Merge branch 'master' into dev-v2.1.0 2022-02-20 11:15:49 +01:00
Gunnar Skjold
7e41168606 Fix for unstable ESP8266 when debug is disabled 2022-02-20 10:51:19 +01:00
Gunnar Skjold
0af2378d05 Merge branch 'master' into dev-v2.1.0 2022-02-19 10:33:17 +01:00
Gunnar Skjold
7f4498c062 Send system values to MQTT on regular interval 2022-02-19 10:32:23 +01:00
Gunnar Skjold
6e9e988ff0 Fixed issue where HAN stops working when debugging is disabled 2022-02-19 10:27:24 +01:00
Gunnar Skjold
1f03013c98 Add possibility to factory reset on boot 2022-02-19 09:57:48 +01:00
Gunnar Skjold
6690e809fb Fixed reboot issue 2022-02-19 09:57:27 +01:00
Gunnar Skjold
b41dd862b6 Merge branch 'master' into dev-v2.1.0 2022-02-19 09:08:02 +01:00
Gunnar Skjold
04c269d982 Double precision on accumulated values 2022-02-19 09:07:35 +01:00
Gunnar Skjold
f8a86058a1 Adjustments to data storage update 2022-02-12 19:54:11 +01:00
Gunnar Skjold
47dda366e3 Fixed ESP32 build 2022-02-12 11:52:06 +01:00
Gunnar Skjold
0700d22d43 Merge branch 'pull-172' into pr-172 2022-02-12 11:49:07 +01:00
Gunnar Skjold
bf633f20c4 Merge pull request #172 from kng/homeassistant
Homeassistant mqtt discovery, wip
2022-02-12 11:48:43 +01:00
Gunnar Skjold
297681bcff Some adjustments to make HA autodiscover work for ESP8266 2022-02-12 11:43:31 +01:00
Gunnar Skjold
68906b54a6 Merge branch 'homeassistant' of github.com:kng/AmsToMqttBridge into pull-172 2022-02-12 11:14:58 +01:00
Gunnar Skjold
0c1d666afd Improve graph calculation stability 2022-02-12 11:12:48 +01:00
Gunnar Skjold
18a38e4104 Some changes to source file header 2022-02-12 10:28:09 +01:00
Gunnar Skjold
ef0041caa6 Added paypal as funding option 2022-02-12 10:27:27 +01:00
Gunnar Skjold
1ce51f6c85 Added AGPLv3 2022-02-12 10:26:56 +01:00
Gunnar Skjold
c23b2a477e Merge branch 'master' into dev-v2.1.0 2022-02-12 10:16:27 +01:00
Gunnar Skjold
0dd77dbaa8 Fixed styling for export graph 2022-02-12 10:09:03 +01:00
Gunnar Skjold
29524c2123 Reducing stack mem footprint 2022-02-10 10:35:57 +01:00
Gunnar Skjold
e2fb5e2673 Removed exception handling 2022-02-08 08:37:54 +01:00
Gunnar Skjold
751a0edca7 160mhz esp32 and some error handling in loop() 2022-02-07 21:18:27 +01:00
Gunnar Skjold
a8e62e086c Divide current another decimal point for Kamstrup CT meters 2022-02-06 19:33:59 +01:00
Gunnar Skjold
35eb69bebb Trying higher buffer for MQTT 2022-02-06 19:28:32 +01:00
Gunnar Skjold
5c7c0699b2 Added images 2022-02-06 11:29:19 +01:00
Gunnar Skjold
460482a6dc Fixed voltage range for yellow ESP indicator 2022-02-06 11:28:56 +01:00
Gunnar Skjold
28db4873a0 Fixed label 2022-02-06 09:32:35 +01:00
Gunnar Skjold
98de734761 Added energy accounting to config file 2022-02-06 09:16:38 +01:00
Gunnar Skjold
b742c8f3c6 Human readable parity in config file 2022-02-05 21:52:13 +01:00
Gunnar Skjold
4cfd274e52 Upload configuration file 2022-02-05 21:44:28 +01:00
Gunnar Skjold
4f7bd0436a Merge branch 'master' into dev-v2.1.0 2022-02-05 19:50:55 +01:00
Gunnar Skjold
7e31a60000 Support null values in the middle of the payload 2022-02-05 19:46:41 +01:00
Gunnar Skjold
793bc877fc Support null values in the middle of the payload 2022-02-05 19:34:44 +01:00
Gunnar Skjold
5ab6de21dc Added some documentation 2022-02-05 19:18:07 +01:00
Gunnar Skjold
7c5a5bcf65 Config file download 2022-02-05 19:17:38 +01:00
Gunnar Skjold
2e203d7713 Merge branch 'master' into dev-v2.1.0 2022-02-04 17:39:13 +01:00
Gunnar Skjold
db5e242ad8 Various changes 2022-02-04 17:36:30 +01:00
Gunnar Skjold
5be921a342 Various changes 2022-02-04 17:35:47 +01:00
Gunnar Skjold
ddd24b20aa Started implementing loading config from FS 2022-01-30 20:53:03 +01:00
Gunnar Skjold
7c1ee20e7b Commit hash as version 2022-01-30 11:10:30 +01:00
Gunnar Skjold
5f0adc0f10 Commit hash as version 2022-01-30 10:54:51 +01:00
Gunnar Skjold
244f78ad1a Fixed bugs 2022-01-30 10:52:51 +01:00
Gunnar Skjold
9066af8dc7 Added list type 4 for power factor 2022-01-29 20:49:50 +01:00
Gunnar Skjold
5cfb58c2e6 Expose TX power adjustment 2022-01-29 20:34:35 +01:00
Gunnar Skjold
fb8d9e51a6 Option to send raw data frame to MQTT 2022-01-29 19:53:59 +01:00
Gunnar Skjold
77ce5d8e90 Energy accounting 2022-01-29 19:27:48 +01:00
Gunnar Skjold
8c69f9a738 Merge branch 'master' into dev-v2.1.0 2022-01-28 19:00:29 +01:00
Gunnar Skjold
dd87d70876 Free value on fuse size and production capacity 2022-01-28 18:29:50 +01:00
Gunnar Skjold
52992f09ee Stability adjustments 2022-01-28 17:41:48 +01:00
Gunnar Skjold
e8a56d5fc7 Merge branch 'master' into dev-v2.1.0 2022-01-23 20:22:01 +01:00
Gunnar Skjold
6aa02d54c8 Improvements for ENTSO-E 2022-01-23 17:52:10 +01:00
Gunnar Skjold
9dbf9137c7 Merge branch 'master' of github.com:gskjold/AmsToMqttBridge 2022-01-21 17:57:35 +01:00
Gunnar Skjold
c0a9a01b41 Fixed uint problem with current 2022-01-21 17:57:01 +01:00
Gunnar Skjold
7b203b196f Fixed voltage and current labels for esp8266 2022-01-21 17:50:02 +01:00
Gunnar Skjold
a72f02a779 First implementation of energy accounting 2022-01-21 17:37:23 +01:00
Gunnar Skjold
50c06e2cfe Removed ap pin from generic ESP32 2022-01-16 19:38:35 +01:00
Gunnar Skjold
e4d4753181 Fixed incorrect labels for IT/TT current 2022-01-16 17:24:57 +01:00
Gunnar Skjold
04daf551fb Fixed incorrect time diff for price fetch 2022-01-16 17:04:58 +01:00
Gunnar Skjold
4b51f0f235 Only update exchange rate once per day 2022-01-16 10:50:08 +01:00
Gunnar Skjold
5e4ccca663 Fetch prices between 13:30 and 14:00 2022-01-16 10:37:08 +01:00
Gunnar Skjold
6b0ec39759 Various updates 2022-01-16 09:58:13 +01:00
Gunnar Skjold
1f7d845a32 Removed debugging 2022-01-14 07:13:43 +01:00
Gunnar Skjold
6de5b719f3 Various changes 2022-01-14 07:12:08 +01:00
Daniel Ekman
f6d2bef285 reduce discovery code to single loop 2022-01-14 00:01:25 +01:00
Daniel Ekman
c444a108a5 Merge branch 'gskjold:master' into homeassistant 2022-01-13 22:50:25 +01:00
Gunnar Skjold
90859ee6b8 Cleanup 2022-01-09 20:47:33 +01:00
Gunnar Skjold
78dd856a97 Various changes 2022-01-09 20:45:25 +01:00
Gunnar Skjold
9897ccc563 Some adjustments for chrome dark mode 2022-01-08 11:19:18 +01:00
Gunnar Skjold
701e5904c5 Fixed build error on ESP8266 2022-01-08 10:39:03 +01:00
Gunnar Skjold
54ea17d345 Re-organized documents 2022-01-08 10:21:19 +01:00
Gunnar Skjold
327fed6f08 Month plot fix and some other things 2022-01-08 10:13:02 +01:00
Gunnar Skjold
8fe1a1edf2 Fixed checksum error for encrypted meters 2022-01-05 19:25:52 +01:00
Gunnar Skjold
6fd8dc0432 Fixed package timestamp for Kaifa 2022-01-03 13:52:04 +01:00
Gunnar Skjold
f586336ae3 Voltage calculation for Kaifa and other three phase meters not reporting voltage for L2 2022-01-03 09:46:53 +01:00
Gunnar Skjold
1527ee7d45 Timezone conversion for Kaifa 2022-01-03 09:05:04 +01:00
Gunnar Skjold
c543b38829 Timezone conversion for Kaifa 2022-01-03 09:00:19 +01:00
Gunnar Skjold
6d12d7120c Increase accumulated values to 3 decimals 2022-01-03 08:41:58 +01:00
Gunnar Skjold
c1d4ba772e Added extra char for string terminator to wifi config 2022-01-03 08:24:10 +01:00
Gunnar Skjold
a03d4113e7 Added checksum verification for Mbus payload 2022-01-03 08:23:49 +01:00
Gunnar Skjold
8751b6325d Updated example ini 2021-12-28 17:30:19 +01:00
Gunnar Skjold
ff02dd43a9 Deleted internal Timezone lib 2021-12-27 14:15:06 +01:00
Gunnar Skjold
b772fb6323 Issue template config 2021-12-27 14:14:24 +01:00
Daniel Ekman
586c0cba25 include supply volt and temp 2021-12-23 17:34:28 +01:00
Daniel Ekman
18bc6753db publish all disscovery params 2021-12-22 20:20:21 +01:00
Daniel Ekman
b79e6112b1 move energy to separate topic 2021-12-22 17:10:10 +01:00
Gunnar Skjold
f5acfce578 Fixed build error 2021-12-22 09:26:03 +01:00
Gunnar Skjold
4b902e2544 Added some defaults for NTP and DNS 2021-12-22 08:09:46 +01:00
Daniel Ekman
2e4e4328f2 first working discovery 2021-12-21 23:17:29 +01:00
Daniel Ekman
03089e92cb basic stuff implemented 2021-12-21 20:40:17 +01:00
Daniel Ekman
86939d4890 topic contains all fields, null if not used 2021-12-21 15:56:09 +01:00
Daniel Ekman
d7d25083dc flat data messages 2021-12-21 14:21:02 +01:00
Daniel Ekman
92e8beadc0 intial test with HA, copy of json 2021-12-21 13:07:07 +01:00
Gunnar Skjold
217247b28e Temperature sensor graph sort 2021-12-19 10:23:18 +01:00
Gunnar Skjold
4828f5a727 Temperature graph 2021-12-19 09:57:51 +01:00
Gunnar Skjold
062a3634a9 Fixed entso-e debugging 2021-12-19 09:17:57 +01:00
Gunnar Skjold
376cd0cf90 Fixed NULL access 2021-12-19 09:15:06 +01:00
Gunnar Skjold
2a20893a58 Fixed real time calculation issues 2021-12-19 09:14:31 +01:00
Gunnar Skjold
f3dba112de Merge branch 'master' of github.com:gskjold/AmsToMqttBridge 2021-12-18 18:30:22 +01:00
Gunnar Skjold
b037d6bb64 Try at least twice before showing ESP as red 2021-12-18 18:25:31 +01:00
Gunnar Skjold
6f7eacddff Trying to get ESP32S2 to work 2021-12-18 18:24:58 +01:00
Gunnar Skjold
4185411315 Merge pull request #169 from kng/master
Change time.h to timelib.h for 32bit to compile
2021-12-18 17:21:19 +01:00
Daniel Ekman
2a1b5a5f6d Change time.h to timelib.h for 32bit to compile 2021-12-18 17:10:53 +01:00
Gunnar Skjold
246a4f96fe Merge pull request #168 from kng/master
Assignment error in meter types
2021-12-18 15:43:09 +01:00
Daniel Ekman
c7d235b367 Assignment error in meter types 2021-12-18 15:37:30 +01:00
Gunnar Skjold
c5c8fbc2a0 Trying timezone from repo again 2021-12-18 15:25:06 +01:00
Gunnar Skjold
8adf591c4e Fixed timelib 2021-12-18 15:20:15 +01:00
Gunnar Skjold
aa893f7ede Fixed bug in price fetch 2021-12-18 15:17:34 +01:00
Gunnar Skjold
f8b1725e94 Revert lib changes 2021-12-18 14:41:58 +01:00
Gunnar Skjold
ae7e3d11d5 Merge branch 'master' of github.com:gskjold/AmsToMqttBridge 2021-12-18 14:33:59 +01:00
Gunnar Skjold
73b20a0766 Merge pull request #167 from kng/master
Libdeps updated to avoid conflict with time.h
2021-12-18 14:29:58 +01:00
Daniel Ekman
d8cf961258 Libdeps updated to avoid conflict with time.h 2021-12-18 13:55:57 +01:00
Gunnar Skjold
bccd19812d Added power factor to MQTT JSON 2021-12-16 11:37:00 +01:00
Gunnar Skjold
6954ff5432 Merge pull request #163 from mikkle/pf-mqtt-cp-fix
Fix c/p error in mqtt powerfactor publishing
2021-12-16 11:26:01 +01:00
Mikkel Troest
6200f31b83 Fix c/p error in mqtt powerfactor publishing 2021-12-16 10:36:38 +01:00
Gunnar Skjold
5408b3c2a9 Make sure temperatures are read just after receiving HAN data 2021-12-13 20:57:17 +01:00
145 changed files with 5744 additions and 3931 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
custom: ["https://paypal.me/gskjold"]

View File

@@ -8,7 +8,7 @@ assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
A clear description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
@@ -18,7 +18,7 @@ Steps to reproduce the behavior:
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
A clear description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
@@ -32,6 +32,7 @@ If applicable, add screenshots to help explain your problem.
- Version: [e.g. 1.5.0]
- MQTT: [yes/no]
- HAN GPIO: [e.g. GPIO5]
- HAN baud and parity: [e.g. 2400 8E1]
- Temperature sensors [e.g. 3xDS18B20]
**Additional context**

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Meter configuration
url: https://github.com/gskjold/AmsToMqttBridge/wiki/Known-hardware-configurations
about: Please check your meter configuration here first.
- name: Frequently asked questions
url: https://github.com/gskjold/AmsToMqttBridge/wiki/FAQ
about: Please check frequently asked questions first.

View File

@@ -32,16 +32,14 @@ jobs:
with:
path: ~/.pio/libdeps
key: ${{ runner.os }}-pio-${{ hashFiles('platformio.ini') }}
- name: Set up Python 3.7
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.7
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -U platformio css_html_js_minify
- name: Configure build targets
run: echo "[platformio]\ndefault_envs = esp8266, esp32" > platformio-user.ini
- name: PlatformIO lib install
run: pio lib install
- name: PlatformIO run

View File

@@ -33,10 +33,10 @@ jobs:
with:
path: ~/.pio/libdeps
key: ${{ runner.os }}-pio-${{ hashFiles('platformio.ini') }}
- name: Set up Python 3.7
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.7
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
@@ -45,6 +45,8 @@ jobs:
run: pio lib install
- name: PlatformIO run
run: pio run
- name: Create zip files
run: /bin/sh scripts/mkzip.sh
- name: Create Release
id: create_release
uses: actions/create-release@v1.0.0
@@ -55,6 +57,35 @@ jobs:
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: Upload esp8266 zip to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: esp8266.zip
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Upload esp32 zip to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: esp32.zip
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Upload esp32s2 zip to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: esp32s2.zip
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Upload esp8266 binary to release
uses: actions/upload-release-asset@v1
env:
@@ -73,12 +104,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 esp32 partitions to release
- name: Upload esp32s2 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp32/partitions.bin
asset_name: ams2mqtt-esp32-partitions-${{ steps.release_tag.outputs.tag }}.bin
asset_path: .pio/build/esp32s2/firmware.bin
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream

2
.gitignore vendored
View File

@@ -13,3 +13,5 @@ platformio-user.ini
/test
/web/test.html
/sdkconfig
/.tmp
/*.zip

617
LICENSE Normal file
View File

@@ -0,0 +1,617 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

Binary file not shown.

View File

@@ -2,7 +2,7 @@
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.0.0.1.255 - Meter number 1
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

BIN
doc/Encrypted payload.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Binary file not shown.

BIN
doc/M-Bus_DOC48.PDF Normal file

Binary file not shown.

BIN
doc/Norway/Kaifa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

View File

@@ -3,7 +3,9 @@
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
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

51
frames/Kamstrup-1p.raw Normal file
View File

@@ -0,0 +1,51 @@
7E A0 BA 2B 21 13 ED AA E6 E7 00 0F 00 00 00 00
0C 07 E6 02 05 06 0D 00 0A FF 80 00 00
02 19
0A 0E 4B 61 6D 73 74 72 75 70 5F 56 30 30 30 31
09 06 01 01 00 00 05 FF 0A 10 35 37 30 36 35 36 37 32 37 31 35 33 33 32 30 37
09 06 01 01 60 01 01 FF 0A 12 36 38 36 31 31 31 31 42 4E 32 34 32 31 30 31 30 34 30
09 06 01 01 01 07 00 FF 06 00 00 02 68
09 06 01 01 02 07 00 FF 06 00 00 00 00
09 06 01 01 03 07 00 FF 06 00 00 00 53
09 06 01 01 04 07 00 FF 06 00 00 00 00
09 06 01 01 1F 07 00 FF 06 00 00 01 22
00 00 00 00
09 06 01 01 20 07 00 FF 12 00 E2
00 00 00 00
05 D8 7E
7E A0 BA 2B 21 13 ED AA E6 E7 00 0F 00 00 00 00
0C 07 E6 02 05 06 0D 00 14 FF 80 00 00
02 19
0A 0E 4B 61 6D 73 74 72 75 70 5F 56 30 30 30 31
09 06 01 01 00 00 05 FF 0A 10 35 37 30 36 35 36 37 32 37 31 35 33 33 32 30 37
09 06 01 01 60 01 01 FF 0A 12 36 38 36 31 31 31 31 42 4E 32 34 32 31 30 31 30 34 30
09 06 01 01 01 07 00 FF 06 00 00 02 68
09 06 01 01 02 07 00 FF 06 00 00 00 00
09 06 01 01 03 07 00 FF 06 00 00 00 53
09 06 01 01 04 07 00 FF 06 00 00 00 00
09 06 01 01 1F 07 00 FF 06 00 00 01 23
00 00 00 00
09 06 01 01 20 07 00 FF 12 00 E1
00 00 00 00
8E 5E 7E
7E A1 04 2B 21 13 77 6E E6 E7 00 0F 00 00 00 00
0C 07 E6 02 05 06 0D 00 19 FF 80 00 00
02 23 0A 0E 4B 61 6D 73 74 72 75 70 5F 56 30 30 30 31
09 06 01 01 00 00 05 FF 0A 10 35 37 30 36 35 36 37 32 37 31 35 33 33 32 30 37
09 06 01 01 60 01 01 FF 0A 12 36 38 36 31 31 31 31 42 4E 32 34 32 31 30 31 30 34 30
09 06 01 01 01 07 00 FF 06 00 00 02 6B
09 06 01 01 02 07 00 FF 06 00 00 00 00
09 06 01 01 03 07 00 FF 06 00 00 00 54
09 06 01 01 04 07 00 FF 06 00 00 00 00
09 06 01 01 1F 07 00 FF 06 00 00 01 25
00 00 00 00
09 06 01 01 20 07 00 FF 12 00 E1
00 00 00 00
09 06 00 01 01 00 00 FF 09 0C 07 E6 02 05 06 0D 00 19 FF 80 00 00
09 06 01 01 01 08 00 FF 06 00 12 CF 93
09 06 01 01 02 08 00 FF 06 00 00 00 00
09 06 01 01 03 08 00 FF 06 00 00 8C CE
09 06 01 01 04 08 00 FF 06 00 05 E5 04
7F E9 7E

View File

@@ -0,0 +1,23 @@
7E A1 2C 2B 21 13 FC 04 E6 E7 00 0F 00 00 00 00
0C 07 E6 02 12 05 16 00 23 FF 80 00 00
02 23
0A 0E 4B 61 6D 73 74 72 75 70 5F 56 30 30 30 31
09 06 01 01 00 00 05 FF 0A 10 35 37 30 36 35 36 37 33 33 31 33 32 33 35 32 36
09 06 01 01 60 01 01 FF 0A 12 36 38 34 31 31 33 31 42 4E 32 34 35 31 30 31 30 39 32
09 06 01 01 01 07 00 FF 06 00 00 08 DD
09 06 01 01 02 07 00 FF 06 00 00 00 00
09 06 01 01 03 07 00 FF 06 00 00 00 00
09 06 01 01 04 07 00 FF 06 00 00 01 90
09 06 01 01 1F 07 00 FF 06 00 00 01 1F
09 06 01 01 33 07 00 FF 06 00 00 02 57
09 06 01 01 47 07 00 FF 06 00 00 00 6A
09 06 01 01 20 07 00 FF 12 00 F1
09 06 01 01 34 07 00 FF 12 00 F0
09 06 01 01 48 07 00 FF 12 00 F3
09 06 00 01 01 00 00 FF 09 0C 07 E6 02 12 05 16 00 23 FF 80 00 00
09 06 01 01 01 08 00 FF 06 01 E8 7F 4E
09 06 01 01 02 08 00 FF 06 00 00 00 00
09 06 01 01 03 08 00 FF 06 00 58 71 8B
09 06 01 01 04 08 00 FF 06 00 10 60 23
B8 1C
7E

View File

@@ -5,8 +5,8 @@ A1 E9 // Frame type and size
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
01 D0 // Length 464
30 // Security tag 0011 0000, 0=Compression off, 0=Unicast, 1=Encryption, 1=Authentication, 0000= Security Suite ID
00 00 A3 2F // Frame counter
// Decrypted frame below

View File

@@ -19,86 +19,27 @@ F8 // Length (248), starting from 0xDB and including end byte
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
68 01 01 68
53 FF 10 01 67
DB
08 53 41 47 59 05 E6 D9 FD
81 F8
20
00 01 A0 E0
0F 80 3E 37 71
0C 07 E5 0C 1B 01 0E 00 2D 00 FF C4 02 // Frame timestamp
02 23 // 35 items
09 0C 07 E5 0C 1B 01 0E 00 2D 00 FF C4 02 // Meter timestamp
09 06 01 00 01 08 00 FF 06 00 43 3D 0A 02 02 0F 00 16 1E
09 06 01 00 02 08 00 FF 06 00 00 01 03 02 02 0F 00 16 1E
09 06 01 00 01 07 00 FF 06 00 00 01 FE 02 02 0F 00 16 1B
09 06 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B
09 06 01 00 20 07 00 FF 12 09 34 02 02 0F FF 16 23
09 06 01 00 34 07 00 FF 12 09 34 02 02 0F FF 16 23
09 06 01 00 48 07 00 FF 12 09 2D 02 02 0F FF 16 23
09 06 01 00 1F 07 00 FF 12 00 63 02 02 0F FE 16 21
09 06 01 00 33 07 00 FF 12 00 3F 02 02 0F FE 16 21
09 06 01 00 47 07 00 FF 12 00 54 02 02 0F FE 16 21
09 06 01 00 0D 07 00 FF 10 03 CF 02 02 0F FD 16 FF // Power factor
09 0C 31 37 38 32 31 30 30 31 35 31 36 35 // Meter ID
01 67

BIN
images/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
images/dayplot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
images/main-header.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
images/monthplot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
images/sensor-displays.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
images/status-bar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
images/tempsensors.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,18 +0,0 @@
# A GitHub Manifesto
### Notes on contributing to my repositories
Jack Christensen
Jan 2018
Surely Git and GitHub are wonderful tools. They make coding and collaboration so much easier. I'm equally impressed with the open source movement, and with the Arduino ecosystem in particular.
I'm just one guy, mostly a hobbyist. Posting my projects to GitHub is my way of giving back a little to the community. It's very gratifying that some of my code has received a modicum of popularity.
Like many things, this has been somewhat of a double-edged sword. Especially since I tend to be a pretty busy guy with many varied interests.
First, I am always interested in bug reports. Please raise an issue in the appropriate repository and please please please include a good, concise description of the issue and a Short, Self Contained, Correct (Compilable), Example (see [sscce.org](http://www.sscce.org/)). I will need to be able to reproduce the issue, with minimal hardware, and without installing a dozen other libraries. I work exclusively with the AVR architecture so most times I will not be able to reproduce issues on other platforms. (There have been one or two occasions where relatively simple changes have been made to accommodate another platform; I am not necessarily averse to these.)
Second, bug reports should always be for problems with *my* code. I will not use GitHub to help you with *your* code, even if you happen to be using one of my libraries. Please use the [Arduino forum](https://forum.arduino.cc/) or other such venue instead.
Finally, pull requests can be problematic, especially if they represent enhancements rather than fixes. I seldom intend my code to be all things to all people. This is mostly a hobby activity and I have very limited bandwidth. Reviewing and managing PRs requires time that I do not often have. Sometimes a PR will take a library in a direction that I'm not interested in. Sometimes a PR will be counter to my original design intent. No doubt the author of a PR thinks that their new feature is the best thing since canned beer, but if I don't happen to share that opinion, then I'll decline it. OTOH, I am certainly capable of making stupid mistakes and missing absolutely fundamental things, and I do appreciate it when these are pointed out.
All this to say, that if I do decline a request, please do not take it personally. Feel free to consider it my problem, not yours. At the end of the day, it's my code, and I reserve the right to decline issues or PRs for any reason, or for no reason at all. But here is the beauty of open source. You can always fork the repository and have your way with it.

View File

@@ -1,675 +0,0 @@
### GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
### Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom
to share and change all versions of a program--to make sure it remains
free software for all its users. We, the Free Software Foundation, use
the GNU General Public License for most of our software; it applies
also to any other work released this way by its authors. You can apply
it to your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you
have certain responsibilities if you distribute copies of the
software, or if you modify it: responsibilities to respect the freedom
of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the
manufacturer can do so. This is fundamentally incompatible with the
aim of protecting users' freedom to change the software. The
systematic pattern of such abuse occurs in the area of products for
individuals to use, which is precisely where it is most unacceptable.
Therefore, we have designed this version of the GPL to prohibit the
practice for those products. If such problems arise substantially in
other domains, we stand ready to extend this provision to those
domains in future versions of the GPL, as needed to protect the
freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish
to avoid the special danger that patents applied to a free program
could make it effectively proprietary. To prevent this, the GPL
assures that patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
### TERMS AND CONDITIONS
#### 0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds
of works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of
an exact copy. The resulting work is called a "modified version" of
the earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user
through a computer network, with no transfer of a copy, is not
conveying.
An interactive user interface displays "Appropriate Legal Notices" to
the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
#### 1. Source Code.
The "source code" for a work means the preferred form of the work for
making modifications to it. "Object code" means any non-source form of
a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users can
regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same
work.
#### 2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey,
without conditions so long as your license otherwise remains in force.
You may convey covered works to others for the sole purpose of having
them make modifications exclusively for you, or provide you with
facilities for running those works, provided that you comply with the
terms of this License in conveying all material for which you do not
control copyright. Those thus making or running the covered works for
you must do so exclusively on your behalf, under your direction and
control, on terms that prohibit them from making any copies of your
copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the
conditions stated below. Sublicensing is not allowed; section 10 makes
it unnecessary.
#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such
circumvention is effected by exercising rights under this License with
respect to the covered work, and you disclaim any intention to limit
operation or modification of the work as a means of enforcing, against
the work's users, your or third parties' legal rights to forbid
circumvention of technological measures.
#### 4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
#### 5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these
conditions:
- a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
- b) The work must carry prominent notices stating that it is
released under this License and any conditions added under
section 7. This requirement modifies the requirement in section 4
to "keep intact all notices".
- c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
- d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
#### 6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of
sections 4 and 5, provided that you also convey the machine-readable
Corresponding Source under the terms of this License, in one of these
ways:
- a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
- b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the Corresponding
Source from a network server at no charge.
- c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
- d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
- e) Convey the object code using peer-to-peer transmission,
provided you inform other peers where the object code and
Corresponding Source of the work are being offered to the general
public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal,
family, or household purposes, or (2) anything designed or sold for
incorporation into a dwelling. In determining whether a product is a
consumer product, doubtful cases shall be resolved in favor of
coverage. For a particular product received by a particular user,
"normally used" refers to a typical or common use of that class of
product, regardless of the status of the particular user or of the way
in which the particular user actually uses, or expects or is expected
to use, the product. A product is a consumer product regardless of
whether the product has substantial commercial, industrial or
non-consumer uses, unless such uses represent the only significant
mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to
install and execute modified versions of a covered work in that User
Product from a modified version of its Corresponding Source. The
information must suffice to ensure that the continued functioning of
the modified object code is in no case prevented or interfered with
solely because modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or
updates for a work that has been modified or installed by the
recipient, or for the User Product in which it has been modified or
installed. Access to a network may be denied when the modification
itself materially and adversely affects the operation of the network
or violates the rules and protocols for communication across the
network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
#### 7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders
of that material) supplement the terms of this License with terms:
- a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
- b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
- c) Prohibiting misrepresentation of the origin of that material,
or requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
- d) Limiting the use for publicity purposes of names of licensors
or authors of the material; or
- e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
- f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions
of it) with contractual assumptions of liability to the recipient,
for any liability that these contractual assumptions directly
impose on those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions; the
above requirements apply either way.
#### 8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your license
from a particular copyright holder is reinstated (a) provisionally,
unless and until the copyright holder explicitly and finally
terminates your license, and (b) permanently, if the copyright holder
fails to notify you of the violation by some reasonable means prior to
60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
#### 9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run
a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
#### 10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
#### 11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned
or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within the
scope of its coverage, prohibits the exercise of, or is conditioned on
the non-exercise of one or more of the rights that are specifically
granted under this License. You may not convey a covered work if you
are a party to an arrangement with a third party that is in the
business of distributing software, under which you make payment to the
third party based on the extent of your activity of conveying the
work, and under which the third party grants, to any of the parties
who would receive the covered work from you, a discriminatory patent
license (a) in connection with copies of the covered work conveyed by
you (or copies made from those copies), or (b) primarily for and in
connection with specific products or compilations that contain the
covered work, unless you entered into that arrangement, or that patent
license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
#### 12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under
this License and any other pertinent obligations, then as a
consequence you may not convey it at all. For example, if you agree to
terms that obligate you to collect a royalty for further conveying
from those to whom you convey the Program, the only way you could
satisfy both those terms and this License would be to refrain entirely
from conveying the Program.
#### 13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
#### 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions
of the GNU General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in
detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies that a certain numbered version of the GNU General Public
License "or any later version" applies to it, you have the option of
following the terms and conditions either of that numbered version or
of any later version published by the Free Software Foundation. If the
Program does not specify a version number of the GNU General Public
License, you may choose any version ever published by the Free
Software Foundation.
If the Program specifies that a proxy can decide which future versions
of the GNU General Public License can be used, that proxy's public
statement of acceptance of a version permanently authorizes you to
choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
#### 15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.
#### 16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
#### 17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
### How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these
terms.
To do so, attach the following notices to the program. It is safest to
attach them to the start of each source file to most effectively state
the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper
mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands \`show w' and \`show c' should show the
appropriate parts of the General Public License. Of course, your
program's commands might be different; for a GUI interface, you would
use an "about box".
You should also get your employer (if you work as a programmer) or
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. For more information on this, and how to apply and follow
the GNU GPL, see <https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your
program into proprietary programs. If your program is a subroutine
library, you may consider it more useful to permit linking proprietary
applications with the library. If this is what you want to do, use the
GNU Lesser General Public License instead of this License. But first,
please read <https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -1,196 +0,0 @@
# Arduino Timezone Library
https://github.com/JChristensen/Timezone
README file
Jack Christensen
Mar 2012
## License
Arduino Timezone Library Copyright (C) 2018 Jack Christensen GNU GPL v3.0
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License v3.0 as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/gpl.html>
## Introduction
The **Timezone** library is designed to work in conjunction with the [Arduino Time library](https://github.com/PaulStoffregen/Time), which must also be installed on your system. This documentation assumes some familiarity with the Time library.
The primary aim of the **Timezone** library is to convert Universal Coordinated Time (UTC) to the correct local time, whether it is daylight saving time (a.k.a. summer time) or standard time. The time source could be a GPS receiver, an NTP server, or a Real-Time Clock (RTC) set to UTC. But whether a hardware RTC or other time source is even present is immaterial, since the Time library can function as a software RTC without additional hardware (although its accuracy is dependent on the accuracy of the microcontroller's system clock.)
The **Timezone** library implements two objects to facilitate time zone conversions:
- A **TimeChangeRule** object describes when local time changes to daylight (summer) time, or to standard time, for a particular locale.
- A **Timezone** object uses **TimeChangeRule**s to perform conversions and related functions. It can also write its **TimeChangeRule**s to EEPROM, or read them from EEPROM. Multiple time zones can be represented by defining multiple **Timezone** objects.
## Examples
The following example sketches are included with the **Timezone** library:
- **Clock:** A simple self-adjusting clock for a single time zone. **TimeChangeRule**s may be optionally read from EEPROM.
- **HardwareRTC:** A self-adjusting clock for one time zone using an external real-time clock, either a DS1307 or DS3231 (e.g. Chronodot) which is set to UTC.
- **WorldClock:** A self-adjusting clock for multiple time zones.
- **WriteRules:** A sketch to write **TimeChangeRule**s to EEPROM.
- **Change_TZ_1:** Changes between time zones by modifying the TimeChangeRules.
- **Change_TZ_2:** Changes between time zones by selecting from an array of Timezone objects.
## Coding TimeChangeRules
Normally these will be coded in pairs for a given time zone: One rule to describe when daylight (summer) time starts, and one to describe when standard time starts.
As an example, here in the Eastern US time zone, Eastern Daylight Time (EDT) starts on the 2nd Sunday in March at 02:00 local time. Eastern Standard Time (EST) starts on the 1st Sunday in November at 02:00 local time.
Define a **TimeChangeRule** as follows:
`TimeChangeRule myRule = {abbrev, week, dow, month, hour, offset};`
Where:
**abbrev** is a character string abbreviation for the time zone; it must be no longer than five characters.
**week** is the week of the month that the rule starts.
**dow** is the day of the week that the rule starts.
**hour** is the hour in local time that the rule starts (0-23).
**offset** is the UTC offset _in minutes_ for the time zone being defined.
For convenience, the following symbolic names can be used:
**week:** First, Second, Third, Fourth, Last
**dow:** Sun, Mon, Tue, Wed, Thu, Fri, Sat
**month:** Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec
For the Eastern US time zone, the **TimeChangeRule**s could be defined as follows:
```c++
TimeChangeRule usEDT = {"EDT", Second, Sun, Mar, 2, -240}; //UTC - 4 hours
TimeChangeRule usEST = {"EST", First, Sun, Nov, 2, -300}; //UTC - 5 hours
```
## Coding Timezone objects
There are three ways to define **Timezone** objects.
By first defining **TimeChangeRule**s (as above) and giving the daylight time rule and the standard time rule (assuming usEDT and usEST defined as above):
`Timezone usEastern(usEDT, usEST);`
For a time zone that does not change to daylight/summer time, pass a single rule to the constructor. For example:
`Timezone usAZ(usMST, usMST);`
By reading rules previously stored in EEPROM. This reads both the daylight and standard time rules previously stored at EEPROM address 100:
`Timezone usPacific(100);`
Note that **TimeChangeRule**s require 12 bytes of storage each, so the pair of rules associated with a Timezone object requires 24 bytes total. This could possibly change in future versions of the library. The size of a **TimeChangeRule** can be checked with `sizeof(usEDT)`.
## Timezone library methods
Note that the `time_t` data type is defined by the Arduino Time library <TimeLib.h>. See the Time library documentation [here](https://playground.arduino.cc/Code/Time) and [here](https://www.pjrc.com/teensy/td_libs_Time.html) for additional details.
### time_t toLocal(time_t utc);
##### Description
Converts the given UTC time to local time, standard or daylight as appropriate.
##### Syntax
`myTZ.toLocal(utc);`
##### Parameters
***utc:*** Universal Coordinated Time *(time_t)*
##### Returns
Local time *(time_t)*
##### Example
```c++
time_t eastern, utc;
TimeChangeRule usEDT = {"EDT", Second, Sun, Mar, 2, -240}; //UTC - 4 hours
TimeChangeRule usEST = {"EST", First, Sun, Nov, 2, -300}; //UTC - 5 hours
Timezone usEastern(usEDT, usEST);
utc = now(); //current time from the Time Library
eastern = usEastern.toLocal(utc);
```
### time_t toLocal(time_t utc, TimeChangeRule **tcr);
##### Description
As above, converts the given UTC time to local time, and also returns a pointer to the **TimeChangeRule** that was applied to do the conversion. This could then be used, for example, to include the time zone abbreviation as part of a time display. The caller must take care not to alter the pointed **TimeChangeRule**, as this will then result in incorrect conversions.
##### Syntax
`myTZ.toLocal(utc, &tcr);`
##### Parameters
***utc:*** Universal Coordinated Time *(time_t)*
***tcr:*** Address of a pointer to a **TimeChangeRule** _(\*\*TimeChangeRule)_
##### Returns
Local time *(time_t)*
Pointer to **TimeChangeRule** _(\*\*TimeChangeRule)_
##### Example
```c++
time_t eastern, utc;
TimeChangeRule *tcr;
TimeChangeRule usEDT = {"EDT", Second, Sun, Mar, 2, -240}; //UTC - 4 hours
TimeChangeRule usEST = {"EST", First, Sun, Nov, 2, -300}; //UTC - 5 hours
Timezone usEastern(usEDT, usEST);
utc = now(); //current time from the Time Library
eastern = usEastern.toLocal(utc, &tcr);
Serial.print("The time zone is: ");
Serial.println(tcr -> abbrev);
```
### bool utcIsDST(time_t utc);
### bool locIsDST(time_t local);
##### Description
These functions determine whether a given UTC time or a given local time is within the daylight saving (summer) time interval, and return true or false accordingly.
##### Syntax
`utcIsDST(utc);`
`locIsDST(local);`
##### Parameters
***utc:*** Universal Coordinated Time *(time_t)*
***local:*** Local Time *(time_t)*
##### Returns
true or false *(bool)*
##### Example
`if (usEastern.utcIsDST(utc)) { /*do something*/ }`
### void readRules(int address);
### void writeRules(int address);
##### Description
These functions read or write a **Timezone** object's two **TimeChangeRule**s from or to EEPROM.
##### Syntax
`myTZ.readRules(address);`
`myTZ.writeRules(address);`
##### Parameters
***address:*** The beginning EEPROM address to write to or read from *(int)*
##### Returns
None.
##### Example
`usEastern.writeRules(100); //write rules beginning at EEPROM address 100`
### void setRules(TimeChangeRule dstStart, TimeChangeRule stdStart);
##### Description
This function reads or updates the daylight and standard time rules from RAM. Can be used to change TimeChangeRules dynamically while a sketch runs.
##### Syntax
`myTZ.setRules(dstStart, stdStart);`
##### Parameters
***dstStart:*** A TimeChangeRule denoting the start of daylight saving (summer) time.
***stdStart:*** A TimeChangeRule denoting the start of standard time.
##### Returns
None.
##### Example
```c++
TimeChangeRule EDT = {"EDT", Second, Sun, Mar, 2, -240};
TimeChangeRule EST = {"EST", First, Sun, Nov, 2, -300};
Timezone ET(EDT, EST);
...
tz.setRules(EDT, EST);
```
### time_t toUTC(time_t local);
##### Description
Converts the given local time to UTC time.
**WARNING:** This function is provided for completeness, but should seldom be needed and should be used sparingly and carefully.
Ambiguous situations occur after the Standard-to-DST and the DST-to-Standard time transitions. When changing to DST, there is one hour of local time that does not exist, since the clock moves forward one hour. Similarly, when changing to standard time, there is one hour of local time that occurs twice since the clock moves back one hour.
This function does not test whether it is passed an erroneous time value during the Local-to-DST transition that does not exist. If passed such a time, an incorrect UTC time value will be returned.
If passed a local time value during the DST-to-Local transition that occurs twice, it will be treated as the earlier time, i.e. the time that occurs before the transition.
Calling this function with local times during a transition interval should be avoided!
##### Syntax
`myTZ.toUTC(local);`
##### Parameters
***local:*** Local Time *(time_t)*
##### Returns
UTC *(time_t)*

View File

@@ -1,119 +0,0 @@
// Arduino Timezone Library Copyright (C) 2018 by Jack Christensen and
// licensed under GNU GPL v3.0, https://www.gnu.org/licenses/gpl.html
//
// Arduino Timezone Library example sketch.
// Demonstrates changing timezone "on the fly".
// Uses a pushbutton switch to change between the four continental US time zones.
// The current timezone setting is saved in EEPROM so it is remembered if
// the power is cycled.
// Tested with Arduino 1.8.5 and an Arduino Uno.
//
// Jack Christensen 02Jan2018
#include <avr/eeprom.h>
#include <JC_Button.h> // http://github.com/JChristensen/JC_Button
#include <Streaming.h> // http://arduiniana.org/libraries/streaming/
#include <Timezone.h> // http://github.com/JChristensen/Timezone
const uint8_t BUTTON_PIN(8); // connect a button from this pin to ground
Button btn(BUTTON_PIN);
uint8_t tzIndex; //index to the arrays below
EEMEM uint8_t ee_tzIndex; //copy of tzIndex persisted in EEPROM
const char* dstNames[] = {"EDT", "CDT", "MDT", "PDT"};
const char* stdNames[] = {"EST", "CST", "MST", "PST"};
const int dstOffsets[] = {-240, -300, -360, -420};
const int stdOffsets[] = {-300, -360, -420, -480};
TimeChangeRule dstRule = {"EDT", Second, Sun, Mar, 2, -240};
TimeChangeRule stdRule = {"EST", First, Sun, Nov, 2, -300};
Timezone tz(dstRule, stdRule);
void setup()
{
// set the system time to UTC
// warning: assumes that compileTime() returns US EST
// adjust the following line accordingly if you're in another time zone
setTime(compileTime() + 300 * 60);
// get tzIndex from eeprom and ensure that it's valid
tzIndex = eeprom_read_byte( &ee_tzIndex );
if ( tzIndex >= sizeof(stdOffsets) / sizeof(stdOffsets[0]) )
{
tzIndex = 0;
eeprom_write_byte( &ee_tzIndex, tzIndex);
}
btn.begin();
Serial.begin(115200);
changeTZ();
}
void loop()
{
// print the time if it's changed
static time_t tLast;
time_t t = now();
if (t != tLast)
{
tLast = t;
printDateTime(t);
Serial << " UTC ";
TimeChangeRule* tcr; //pointer to current time change rule, used to get TZ abbrev
printDateTime(tz.toLocal(t, &tcr));
Serial << " " << tcr -> abbrev;
Serial << endl;
}
// change the time zone if button pressed
btn.read();
if (btn.wasPressed())
{
if ( ++tzIndex >= sizeof(stdOffsets) / sizeof(stdOffsets[0]) ) tzIndex = 0;
changeTZ();
}
}
void changeTZ()
{
Serial << "tzIndex " << tzIndex << endl;
eeprom_update_byte( &ee_tzIndex, tzIndex );
dstRule.offset = dstOffsets[tzIndex];
stdRule.offset = stdOffsets[tzIndex];
strcpy(dstRule.abbrev, dstNames[tzIndex]);
strcpy(stdRule.abbrev, stdNames[tzIndex]);
tz.setRules(dstRule, stdRule);
}
void printDateTime(time_t t)
{
Serial << ((day(t)<10) ? "0" : "") << _DEC(day(t));
Serial << monthShortStr(month(t)) << _DEC(year(t)) << ' ';
Serial << ((hour(t)<10) ? "0" : "") << _DEC(hour(t)) << ':';
Serial << ((minute(t)<10) ? "0" : "") << _DEC(minute(t)) << ':';
Serial << ((second(t)<10) ? "0" : "") << _DEC(second(t));
}
// function to return the compile date and time as a time_t value
time_t compileTime()
{
const time_t FUDGE(10); //fudge factor to allow for upload time, etc. (seconds, YMMV)
const char *compDate = __DATE__, *compTime = __TIME__, *months = "JanFebMarAprMayJunJulAugSepOctNovDec";
char compMon[4], *m;
strncpy(compMon, compDate, 3);
compMon[3] = '\0';
m = strstr(months, compMon);
tmElements_t tm;
tm.Month = ((m - months) / 3 + 1);
tm.Day = atoi(compDate + 4);
tm.Year = atoi(compDate + 7) - 1970;
tm.Hour = atoi(compTime);
tm.Minute = atoi(compTime + 3);
tm.Second = atoi(compTime + 6);
time_t t = makeTime(tm);
return t + FUDGE; //add fudge factor to allow for compile time
}

View File

@@ -1,105 +0,0 @@
// Arduino Timezone Library Copyright (C) 2018 by Jack Christensen and
// licensed under GNU GPL v3.0, https://www.gnu.org/licenses/gpl.html
//
// Arduino Timezone Library example sketch.
// Demonstrates changing time zones using an array of Timezone objects.
// Uses a pushbutton switch to change between the four US continental time zones.
// Tested with Arduino 1.8.5 and an Arduino Uno.
//
// Jack Christensen 02Jan2018
#include <JC_Button.h> // http://github.com/JChristensen/JC_Button
#include <Streaming.h> // http://arduiniana.org/libraries/streaming/
#include <Timezone.h> // http://github.com/JChristensen/Timezone
const uint8_t BUTTON_PIN(8); // connect a button from this pin to ground
Button btn(BUTTON_PIN);
//Continental US Time Zones
TimeChangeRule EDT = { "EDT", Second, Sun, Mar, 2, -240 }; //Daylight time = UTC - 4 hours
TimeChangeRule EST = { "EST", First, Sun, Nov, 2, -300 }; //Standard time = UTC - 5 hours
Timezone Eastern(EDT, EST);
TimeChangeRule CDT = { "CDT", Second, Sun, Mar, 2, -300 }; //Daylight time = UTC - 5 hours
TimeChangeRule CST = { "CST", First, Sun, Nov, 2, -360 }; //Standard time = UTC - 6 hours
Timezone Central(CDT, CST);
TimeChangeRule MDT = { "MDT", Second, Sun, Mar, 2, -360 }; //Daylight time = UTC - 6 hours
TimeChangeRule MST = { "MST", First, Sun, Nov, 2, -420 }; //Standard time = UTC - 7 hours
Timezone Mountain(MDT, MST);
TimeChangeRule PDT = { "PDT", Second, Sun, Mar, 2, -420 }; //Daylight time = UTC - 7 hours
TimeChangeRule PST = { "PST", First, Sun, Nov, 2, -480 }; //Standard time = UTC - 8 hours
Timezone Pacific(PDT, PST);
Timezone* timezones[] = { &Eastern, &Central, &Mountain, &Pacific };
Timezone* tz; //pointer to the time zone
uint8_t tzIndex; //indexes the timezones[] array
TimeChangeRule* tcr; //pointer to the time change rule, use to get TZ abbrev
void setup()
{
// set the system time to UTC
// warning: assumes that compileTime() returns US EST
// adjust the following line accordingly if you're in another time zone
setTime(compileTime() + 300 * 60);
btn.begin();
Serial.begin(115200);
tz = timezones[tzIndex];
}
void loop()
{
// print the time if it's changed
static time_t tLast;
time_t t = now();
if (t != tLast)
{
tLast = t;
printDateTime(t);
Serial << " UTC ";
TimeChangeRule* tcr; //pointer to current time change rule, used to get TZ abbrev
printDateTime((*tz).toLocal(t, &tcr));
Serial << " " << tcr -> abbrev;
Serial << endl;
}
// change the time zone if button pressed
btn.read();
if (btn.wasPressed())
{
if ( ++tzIndex >= sizeof(timezones) / sizeof(timezones[0]) ) tzIndex = 0;
Serial << "tzIndex " << tzIndex << endl;
tz = timezones[tzIndex];
}
}
void printDateTime(time_t t)
{
Serial << ((day(t)<10) ? "0" : "") << _DEC(day(t));
Serial << monthShortStr(month(t)) << _DEC(year(t)) << ' ';
Serial << ((hour(t)<10) ? "0" : "") << _DEC(hour(t)) << ':';
Serial << ((minute(t)<10) ? "0" : "") << _DEC(minute(t)) << ':';
Serial << ((second(t)<10) ? "0" : "") << _DEC(second(t));
}
// function to return the compile date and time as a time_t value
time_t compileTime()
{
const time_t FUDGE(10); //fudge factor to allow for upload time, etc. (seconds, YMMV)
const char *compDate = __DATE__, *compTime = __TIME__, *months = "JanFebMarAprMayJunJulAugSepOctNovDec";
char compMon[4], *m;
strncpy(compMon, compDate, 3);
compMon[3] = '\0';
m = strstr(months, compMon);
tmElements_t tm;
tm.Month = ((m - months) / 3 + 1);
tm.Day = atoi(compDate + 4);
tm.Year = atoi(compDate + 7) - 1970;
tm.Hour = atoi(compTime);
tm.Minute = atoi(compTime + 3);
tm.Second = atoi(compTime + 6);
time_t t = makeTime(tm);
return t + FUDGE; //add fudge factor to allow for compile time
}

View File

@@ -1,71 +0,0 @@
// Arduino Timezone Library Copyright (C) 2018 by Jack Christensen and
// licensed under GNU GPL v3.0, https://www.gnu.org/licenses/gpl.html
//
// Arduino Timezone Library example sketch.
// Self-adjusting clock for one time zone.
// TimeChangeRules can be hard-coded or read from EEPROM, see comments.
// Jack Christensen Mar 2012
#include <Timezone.h> // https://github.com/JChristensen/Timezone
// US Eastern Time Zone (New York, Detroit)
TimeChangeRule myDST = {"EDT", Second, Sun, Mar, 2, -240}; // Daylight time = UTC - 4 hours
TimeChangeRule mySTD = {"EST", First, Sun, Nov, 2, -300}; // Standard time = UTC - 5 hours
Timezone myTZ(myDST, mySTD);
// If TimeChangeRules are already stored in EEPROM, comment out the three
// lines above and uncomment the line below.
//Timezone myTZ(100); // assumes rules stored at EEPROM address 100
TimeChangeRule *tcr; // pointer to the time change rule, use to get TZ abbrev
void setup()
{
Serial.begin(115200);
setTime(myTZ.toUTC(compileTime()));
//setTime(01, 55, 00, 11, 3, 2012); //another way to set the time (hr,min,sec,day,mnth,yr)
}
void loop()
{
time_t utc = now();
time_t local = myTZ.toLocal(utc, &tcr);
Serial.println();
printDateTime(utc, "UTC");
printDateTime(local, tcr -> abbrev);
delay(10000);
}
// Function to return the compile date and time as a time_t value
time_t compileTime()
{
const time_t FUDGE(10); // fudge factor to allow for compile time (seconds, YMMV)
const char *compDate = __DATE__, *compTime = __TIME__, *months = "JanFebMarAprMayJunJulAugSepOctNovDec";
char chMon[4], *m;
tmElements_t tm;
strncpy(chMon, compDate, 3);
chMon[3] = '\0';
m = strstr(months, chMon);
tm.Month = ((m - months) / 3 + 1);
tm.Day = atoi(compDate + 4);
tm.Year = atoi(compDate + 7) - 1970;
tm.Hour = atoi(compTime);
tm.Minute = atoi(compTime + 3);
tm.Second = atoi(compTime + 6);
time_t t = makeTime(tm);
return t + FUDGE; // add fudge factor to allow for compile time
}
// format and print a time_t value, with a time zone appended.
void printDateTime(time_t t, const char *tz)
{
char buf[32];
char m[4]; // temporary storage for month string (DateStrings.cpp uses shared buffer)
strcpy(m, monthShortStr(month(t)));
sprintf(buf, "%.2d:%.2d:%.2d %s %.2d %s %d %s",
hour(t), minute(t), second(t), dayShortStr(weekday(t)), day(t), m, year(t), tz);
Serial.println(buf);
}

View File

@@ -1,57 +0,0 @@
// Arduino Timezone Library Copyright (C) 2018 by Jack Christensen and
// licensed under GNU GPL v3.0, https://www.gnu.org/licenses/gpl.html
//
// Arduino Timezone Library example sketch.
// Self-adjusting clock for one time zone using an external real-time
// clock, either a DS1307 or DS3231 (e.g. Chronodot).
// Assumes the RTC is set to UTC.
// TimeChangeRules can be hard-coded or read from EEPROM, see comments.
// Check out the Chronodot at http://www.macetech.com/store/
//
// Jack Christensen Aug 2012
#include <DS1307RTC.h> // https://github.com/PaulStoffregen/DS1307RTC
#include <Timezone.h> // https://github.com/JChristensen/Timezone
// US Eastern Time Zone (New York, Detroit)
TimeChangeRule myDST = {"EDT", Second, Sun, Mar, 2, -240}; //Daylight time = UTC - 4 hours
TimeChangeRule mySTD = {"EST", First, Sun, Nov, 2, -300}; //Standard time = UTC - 5 hours
Timezone myTZ(myDST, mySTD);
// If TimeChangeRules are already stored in EEPROM, comment out the three
// lines above and uncomment the line below.
//Timezone myTZ(100); //assumes rules stored at EEPROM address 100
TimeChangeRule *tcr; //pointer to the time change rule, use to get TZ abbrev
void setup()
{
Serial.begin(115200);
setSyncProvider(RTC.get); // the function to get the time from the RTC
if(timeStatus()!= timeSet)
Serial.println("Unable to sync with the RTC");
else
Serial.println("RTC has set the system time");
}
void loop()
{
time_t utc = now();
time_t local = myTZ.toLocal(utc, &tcr);
Serial.println();
printDateTime(utc, "UTC");
printDateTime(local, tcr -> abbrev);
delay(10000);
}
// format and print a time_t value, with a time zone appended.
void printDateTime(time_t t, const char *tz)
{
char buf[32];
char m[4]; // temporary storage for month string (DateStrings.cpp uses shared buffer)
strcpy(m, monthShortStr(month(t)));
sprintf(buf, "%.2d:%.2d:%.2d %s %.2d %s %d %s",
hour(t), minute(t), second(t), dayShortStr(weekday(t)), day(t), m, year(t), tz);
Serial.println(buf);
}

View File

@@ -1,123 +0,0 @@
// Arduino Timezone Library Copyright (C) 2018 by Jack Christensen and
// licensed under GNU GPL v3.0, https://www.gnu.org/licenses/gpl.html
//
// Arduino Timezone Library example sketch.
// Self-adjusting clock for multiple time zones.
// Jack Christensen Mar 2012
//
// For time zone information:
// http://www.timeanddate.com/worldclock/
#include <Timezone.h> // https://github.com/JChristensen/Timezone
// Australia Eastern Time Zone (Sydney, Melbourne)
TimeChangeRule aEDT = {"AEDT", First, Sun, Oct, 2, 660}; // UTC + 11 hours
TimeChangeRule aEST = {"AEST", First, Sun, Apr, 3, 600}; // UTC + 10 hours
Timezone ausET(aEDT, aEST);
// Moscow Standard Time (MSK, does not observe DST)
TimeChangeRule msk = {"MSK", Last, Sun, Mar, 1, 180};
Timezone tzMSK(msk);
// 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
Timezone CE(CEST, CET);
// United Kingdom (London, Belfast)
TimeChangeRule BST = {"BST", Last, Sun, Mar, 1, 60}; // British Summer Time
TimeChangeRule GMT = {"GMT", Last, Sun, Oct, 2, 0}; // Standard Time
Timezone UK(BST, GMT);
// UTC
TimeChangeRule utcRule = {"UTC", Last, Sun, Mar, 1, 0}; // UTC
Timezone UTC(utcRule);
// US Eastern Time Zone (New York, Detroit)
TimeChangeRule usEDT = {"EDT", Second, Sun, Mar, 2, -240}; // Eastern Daylight Time = UTC - 4 hours
TimeChangeRule usEST = {"EST", First, Sun, Nov, 2, -300}; // Eastern Standard Time = UTC - 5 hours
Timezone usET(usEDT, usEST);
// US Central Time Zone (Chicago, Houston)
TimeChangeRule usCDT = {"CDT", Second, Sun, Mar, 2, -300};
TimeChangeRule usCST = {"CST", First, Sun, Nov, 2, -360};
Timezone usCT(usCDT, usCST);
// US Mountain Time Zone (Denver, Salt Lake City)
TimeChangeRule usMDT = {"MDT", Second, Sun, Mar, 2, -360};
TimeChangeRule usMST = {"MST", First, Sun, Nov, 2, -420};
Timezone usMT(usMDT, usMST);
// Arizona is US Mountain Time Zone but does not use DST
Timezone usAZ(usMST);
// US Pacific Time Zone (Las Vegas, Los Angeles)
TimeChangeRule usPDT = {"PDT", Second, Sun, Mar, 2, -420};
TimeChangeRule usPST = {"PST", First, Sun, Nov, 2, -480};
Timezone usPT(usPDT, usPST);
void setup()
{
Serial.begin(115200);
// set the system time to UTC
// warning: assumes that compileTime() returns US EDT
// adjust the following line accordingly if you're in another time zone
setTime(compileTime() + 240 * 60);
}
void loop()
{
time_t utc = now();
Serial.println();
printDateTime(ausET, utc, "Sydney");
printDateTime(tzMSK, utc, " Moscow");
printDateTime(CE, utc, "Paris");
printDateTime(UK, utc, " London");
printDateTime(UTC, utc, " Universal Coordinated Time");
printDateTime(usET, utc, " New York");
printDateTime(usCT, utc, " Chicago");
printDateTime(usMT, utc, " Denver");
printDateTime(usAZ, utc, " Phoenix");
printDateTime(usPT, utc, " Los Angeles");
delay(10000);
}
// Function to return the compile date and time as a time_t value
time_t compileTime()
{
const time_t FUDGE(10); // fudge factor to allow for compile time (seconds, YMMV)
const char *compDate = __DATE__, *compTime = __TIME__, *months = "JanFebMarAprMayJunJulAugSepOctNovDec";
char chMon[4], *m;
tmElements_t tm;
strncpy(chMon, compDate, 3);
chMon[3] = '\0';
m = strstr(months, chMon);
tm.Month = ((m - months) / 3 + 1);
tm.Day = atoi(compDate + 4);
tm.Year = atoi(compDate + 7) - 1970;
tm.Hour = atoi(compTime);
tm.Minute = atoi(compTime + 3);
tm.Second = atoi(compTime + 6);
time_t t = makeTime(tm);
return t + FUDGE; // add fudge factor to allow for compile time
}
// given a Timezone object, UTC and a string description, convert and print local time with time zone
void printDateTime(Timezone tz, time_t utc, const char *descr)
{
char buf[40];
char m[4]; // temporary storage for month string (DateStrings.cpp uses shared buffer)
TimeChangeRule *tcr; // pointer to the time change rule, use to get the TZ abbrev
time_t t = tz.toLocal(utc, &tcr);
strcpy(m, monthShortStr(month(t)));
sprintf(buf, "%.2d:%.2d:%.2d %s %.2d %s %d %s",
hour(t), minute(t), second(t), dayShortStr(weekday(t)), day(t), m, year(t), tcr -> abbrev);
Serial.print(buf);
Serial.print(' ');
Serial.println(descr);
}

View File

@@ -1,29 +0,0 @@
// Arduino Timezone Library Copyright (C) 2018 by Jack Christensen and
// licensed under GNU GPL v3.0, https://www.gnu.org/licenses/gpl.html
//
// Arduino Timezone Library example sketch.
// Write TimeChangeRules to EEPROM.
// Jack Christensen Mar 2012
#include <Timezone.h> // https://github.com/JChristensen/Timezone
// US Eastern Time Zone (New York, Detroit)
TimeChangeRule usEdt = {"EDT", Second, Sun, Mar, 2, -240}; // UTC - 4 hours
TimeChangeRule usEst = {"EST", First, Sun, Nov, 2, -300}; // UTC - 5 hours
Timezone usEastern(usEdt, usEst);
void setup()
{
pinMode(13, OUTPUT);
usEastern.writeRules(100); // write rules to EEPROM address 100
}
void loop()
{
// fast blink to indicate EEPROM write is complete
digitalWrite(13, HIGH);
delay(100);
digitalWrite(13, LOW);
delay(100);
}

View File

@@ -1,85 +0,0 @@
// Arduino Timezone Library Copyright (C) 2018 by Jack Christensen and
// licensed under GNU GPL v3.0, https://www.gnu.org/licenses/gpl.html
//
// Arduino Timezone Library example sketch.
// Sketch to verify operation of Timezone library.
// Jack Christensen 14Nov2018
#include <Timezone.h> // https://github.com/JChristensen/Timezone
#include <TimeLib.h> // https://github.com/PaulStoffregen/Time
// New Zealand Time Zone
TimeChangeRule nzSTD = {"NZST", First, Sun, Apr, 3, 720}; // UTC + 12 hours
TimeChangeRule nzDST = {"NZDT", Last, Sun, Sep, 2, 780}; // UTC + 13 hours
Timezone nz(nzDST, nzSTD);
// US Eastern Time Zone (New York, Detroit)
TimeChangeRule etDST = {"EDT", Second, Sun, Mar, 2, -240}; // Daylight time = UTC - 4 hours
TimeChangeRule etSTD = {"EST", First, Sun, Nov, 2, -300}; // Standard time = UTC - 5 hours
Timezone et(etDST, etSTD);
void setup()
{
Serial.begin(115200);
// New Zealand
printTimes( 1, 4, 2018, nzSTD.hour, nzDST.offset, nz); // day, month, year, hour, offset, tz
printTimes(30, 9, 2018, nzDST.hour, nzSTD.offset, nz);
printTimes( 7, 4, 2019, nzSTD.hour, nzDST.offset, nz);
printTimes(29, 9, 2019, nzDST.hour, nzSTD.offset, nz);
printTimes( 5, 4, 2020, nzSTD.hour, nzDST.offset, nz);
printTimes(27, 9, 2020, nzDST.hour, nzSTD.offset, nz);
// US Eastern
printTimes(11, 3, 2018, etDST.hour, etSTD.offset, et); // day, month, year, hour, offset, tz
printTimes( 4, 11, 2018, etSTD.hour, etDST.offset, et);
printTimes(10, 3, 2019, etDST.hour, etSTD.offset, et);
printTimes( 3, 11, 2019, etSTD.hour, etDST.offset, et);
printTimes( 8, 3, 2020, etDST.hour, etSTD.offset, et);
printTimes( 1, 11, 2020, etSTD.hour, etDST.offset, et);
}
void loop() {}
// print corresponding UTC and local times "n" seconds before and after the time change.
// h is the hour to change the clock using the *current* time (i.e. before the change).
// offset is the utc offset in minutes for the time *after* the change.
void printTimes(uint8_t d, uint8_t m, int y, uint8_t h, int offset, Timezone tz)
{
const time_t n(3); // number of times to print before and after the time change
tmElements_t tm;
tm.Hour = h;
tm.Minute = 0;
tm.Second = 0;
tm.Day = d;
tm.Month = m;
tm.Year = y - 1970; // offset from 1970
time_t utc = makeTime(tm) - offset * SECS_PER_MIN - n;
Serial.print(F("\n-------- "));
Serial.print(monthShortStr(m));
Serial.print('-');
Serial.print(y);
Serial.print(F(" time change --------\n"));
for (uint16_t i=0; i<n*2; i++)
{
TimeChangeRule *tcr; // pointer to the time change rule, use to get TZ abbrev
time_t local = tz.toLocal(utc, &tcr);
printDateTime(utc, "UTC = ");
printDateTime(local, tcr -> abbrev);
Serial.println();
++utc;
}
}
// format and print a time_t value, with a time zone appended.
void printDateTime(time_t t, const char *tz)
{
char buf[32];
char m[4]; // temporary storage for month string (DateStrings.cpp uses shared buffer)
strcpy(m, monthShortStr(month(t)));
sprintf(buf, "%.2d:%.2d:%.2d %s %.2d %s %d %s",
hour(t), minute(t), second(t), dayShortStr(weekday(t)), day(t), m, year(t), tz);
Serial.print(buf);
}

View File

@@ -1,9 +0,0 @@
TimeChangeRule KEYWORD1
Timezone KEYWORD1
toLocal KEYWORD2
toUTC KEYWORD2
utcIsDST KEYWORD2
locIsDST KEYWORD2
setRules KEYWORD2
readRules KEYWORD2
writeRules KEYWORD2

View File

@@ -1,10 +0,0 @@
name=Timezone
version=1.2.4
author=Jack Christensen <jack.christensen@outlook.com>
maintainer=Jack Christensen <jack.christensen@outlook.com>
sentence=Arduino library to facilitate time zone conversions and automatic daylight saving (summer) time adjustments.
paragraph=The primary aim of the Timezone library is to convert Universal Coordinated Time (UTC) to the correct local time, whether it is daylight saving time (a.k.a. summer time) or standard time.
category=Timing
url=https://github.com/JChristensen/Timezone
architectures=*
depends=Time (=1.6.0)

View File

@@ -1,243 +0,0 @@
/*----------------------------------------------------------------------*
* Arduino Timezone Library *
* Jack Christensen Mar 2012 *
* *
* Arduino Timezone Library Copyright (C) 2018 by Jack Christensen and *
* licensed under GNU GPL v3.0, https://www.gnu.org/licenses/gpl.html *
*----------------------------------------------------------------------*/
#include "Timezone.h"
#ifdef __AVR__
#include <avr/eeprom.h>
#endif
/*----------------------------------------------------------------------*
* Create a Timezone object from the given time change rules. *
*----------------------------------------------------------------------*/
Timezone::Timezone(TimeChangeRule dstStart, TimeChangeRule stdStart)
: m_dst(dstStart), m_std(stdStart)
{
initTimeChanges();
}
/*----------------------------------------------------------------------*
* Create a Timezone object for a zone that does not observe *
* daylight time. *
*----------------------------------------------------------------------*/
Timezone::Timezone(TimeChangeRule stdTime)
: m_dst(stdTime), m_std(stdTime)
{
initTimeChanges();
}
#ifdef __AVR__
/*----------------------------------------------------------------------*
* Create a Timezone object from time change rules stored in EEPROM *
* at the given address. *
*----------------------------------------------------------------------*/
Timezone::Timezone(int address)
{
readRules(address);
}
#endif
/*----------------------------------------------------------------------*
* Convert the given UTC time to local time, standard or *
* daylight time, as appropriate. *
*----------------------------------------------------------------------*/
time_t Timezone::toLocal(time_t utc)
{
// recalculate the time change points if needed
if (year(utc) != year(m_dstUTC)) calcTimeChanges(year(utc));
if (utcIsDST(utc))
return utc + m_dst.offset * SECS_PER_MIN;
else
return utc + m_std.offset * SECS_PER_MIN;
}
/*----------------------------------------------------------------------*
* Convert the given UTC time to local time, standard or *
* daylight time, as appropriate, and return a pointer to the time *
* change rule used to do the conversion. The caller must take care *
* not to alter this rule. *
*----------------------------------------------------------------------*/
time_t Timezone::toLocal(time_t utc, TimeChangeRule **tcr)
{
// recalculate the time change points if needed
if (year(utc) != year(m_dstUTC)) calcTimeChanges(year(utc));
if (utcIsDST(utc)) {
*tcr = &m_dst;
return utc + m_dst.offset * SECS_PER_MIN;
}
else {
*tcr = &m_std;
return utc + m_std.offset * SECS_PER_MIN;
}
}
/*----------------------------------------------------------------------*
* Convert the given local time to UTC time. *
* *
* WARNING: *
* This function is provided for completeness, but should seldom be *
* needed and should be used sparingly and carefully. *
* *
* Ambiguous situations occur after the Standard-to-DST and the *
* DST-to-Standard time transitions. When changing to DST, there is *
* one hour of local time that does not exist, since the clock moves *
* forward one hour. Similarly, when changing to standard time, there *
* is one hour of local times that occur twice since the clock moves *
* back one hour. *
* *
* This function does not test whether it is passed an erroneous time *
* value during the Local -> DST transition that does not exist. *
* If passed such a time, an incorrect UTC time value will be returned. *
* *
* If passed a local time value during the DST -> Local transition *
* that occurs twice, it will be treated as the earlier time, i.e. *
* the time that occurs before the transistion. *
* *
* Calling this function with local times during a transition interval *
* should be avoided! *
*----------------------------------------------------------------------*/
time_t Timezone::toUTC(time_t local)
{
// recalculate the time change points if needed
if (year(local) != year(m_dstLoc)) calcTimeChanges(year(local));
if (locIsDST(local))
return local - m_dst.offset * SECS_PER_MIN;
else
return local - m_std.offset * SECS_PER_MIN;
}
/*----------------------------------------------------------------------*
* Determine whether the given UTC time_t is within the DST interval *
* or the Standard time interval. *
*----------------------------------------------------------------------*/
bool Timezone::utcIsDST(time_t utc)
{
// recalculate the time change points if needed
if (year(utc) != year(m_dstUTC)) calcTimeChanges(year(utc));
if (m_stdUTC == m_dstUTC) // daylight time not observed in this tz
return false;
else if (m_stdUTC > m_dstUTC) // northern hemisphere
return (utc >= m_dstUTC && utc < m_stdUTC);
else // southern hemisphere
return !(utc >= m_stdUTC && utc < m_dstUTC);
}
/*----------------------------------------------------------------------*
* Determine whether the given Local time_t is within the DST interval *
* or the Standard time interval. *
*----------------------------------------------------------------------*/
bool Timezone::locIsDST(time_t local)
{
// recalculate the time change points if needed
if (year(local) != year(m_dstLoc)) calcTimeChanges(year(local));
if (m_stdUTC == m_dstUTC) // daylight time not observed in this tz
return false;
else if (m_stdLoc > m_dstLoc) // northern hemisphere
return (local >= m_dstLoc && local < m_stdLoc);
else // southern hemisphere
return !(local >= m_stdLoc && local < m_dstLoc);
}
/*----------------------------------------------------------------------*
* Calculate the DST and standard time change points for the given *
* given year as local and UTC time_t values. *
*----------------------------------------------------------------------*/
void Timezone::calcTimeChanges(int yr)
{
m_dstLoc = toTime_t(m_dst, yr);
m_stdLoc = toTime_t(m_std, yr);
m_dstUTC = m_dstLoc - m_std.offset * SECS_PER_MIN;
m_stdUTC = m_stdLoc - m_dst.offset * SECS_PER_MIN;
}
/*----------------------------------------------------------------------*
* Initialize the DST and standard time change points. *
*----------------------------------------------------------------------*/
void Timezone::initTimeChanges()
{
m_dstLoc = 0;
m_stdLoc = 0;
m_dstUTC = 0;
m_stdUTC = 0;
}
/*----------------------------------------------------------------------*
* Convert the given time change rule to a time_t value *
* for the given year. *
*----------------------------------------------------------------------*/
time_t Timezone::toTime_t(TimeChangeRule r, int yr)
{
uint8_t m = r.month; // temp copies of r.month and r.week
uint8_t w = r.week;
if (w == 0) // is this a "Last week" rule?
{
if (++m > 12) // yes, for "Last", go to the next month
{
m = 1;
++yr;
}
w = 1; // and treat as first week of next month, subtract 7 days later
}
// calculate first day of the month, or for "Last" rules, first day of the next month
tmElements_t tm;
tm.Hour = r.hour;
tm.Minute = 0;
tm.Second = 0;
tm.Day = 1;
tm.Month = m;
tm.Year = yr - 1970;
time_t t = makeTime(tm);
// add offset from the first of the month to r.dow, and offset for the given week
t += ( (r.dow - weekday(t) + 7) % 7 + (w - 1) * 7 ) * SECS_PER_DAY;
// back up a week if this is a "Last" rule
if (r.week == 0) t -= 7 * SECS_PER_DAY;
return t;
}
/*----------------------------------------------------------------------*
* Read or update the daylight and standard time rules from RAM. *
*----------------------------------------------------------------------*/
void Timezone::setRules(TimeChangeRule dstStart, TimeChangeRule stdStart)
{
m_dst = dstStart;
m_std = stdStart;
initTimeChanges(); // force calcTimeChanges() at next conversion call
}
#ifdef __AVR__
/*----------------------------------------------------------------------*
* Read the daylight and standard time rules from EEPROM at *
* the given address. *
*----------------------------------------------------------------------*/
void Timezone::readRules(int address)
{
eeprom_read_block((void *) &m_dst, (void *) address, sizeof(m_dst));
address += sizeof(m_dst);
eeprom_read_block((void *) &m_std, (void *) address, sizeof(m_std));
initTimeChanges(); // force calcTimeChanges() at next conversion call
}
/*----------------------------------------------------------------------*
* Write the daylight and standard time rules to EEPROM at *
* the given address. *
*----------------------------------------------------------------------*/
void Timezone::writeRules(int address)
{
eeprom_write_block((void *) &m_dst, (void *) address, sizeof(m_dst));
address += sizeof(m_dst);
eeprom_write_block((void *) &m_std, (void *) address, sizeof(m_std));
}
#endif

View File

@@ -1,61 +0,0 @@
/*----------------------------------------------------------------------*
* Arduino Timezone Library *
* Jack Christensen Mar 2012 *
* *
* Arduino Timezone Library Copyright (C) 2018 by Jack Christensen and *
* licensed under GNU GPL v3.0, https://www.gnu.org/licenses/gpl.html *
*----------------------------------------------------------------------*/
#ifndef TIMEZONE_H_INCLUDED
#define TIMEZONE_H_INCLUDED
#if ARDUINO >= 100
#include <Arduino.h>
#else
#include <WProgram.h>
#endif
#include <TimeLib.h> // https://github.com/PaulStoffregen/Time
// convenient constants for TimeChangeRules
enum week_t {Last, First, Second, Third, Fourth};
enum dow_t {Sun=1, Mon, Tue, Wed, Thu, Fri, Sat};
enum month_t {Jan=1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec};
// structure to describe rules for when daylight/summer time begins,
// or when standard time begins.
struct TimeChangeRule
{
char abbrev[6]; // five chars max
uint8_t week; // First, Second, Third, Fourth, or Last week of the month
uint8_t dow; // day of week, 1=Sun, 2=Mon, ... 7=Sat
uint8_t month; // 1=Jan, 2=Feb, ... 12=Dec
uint8_t hour; // 0-23
int offset; // offset from UTC in minutes
};
class Timezone
{
public:
Timezone(TimeChangeRule dstStart, TimeChangeRule stdStart);
Timezone(TimeChangeRule stdTime);
Timezone(int address);
time_t toLocal(time_t utc);
time_t toLocal(time_t utc, TimeChangeRule **tcr);
time_t toUTC(time_t local);
bool utcIsDST(time_t utc);
bool locIsDST(time_t local);
void setRules(TimeChangeRule dstStart, TimeChangeRule stdStart);
void readRules(int address);
void writeRules(int address);
private:
void calcTimeChanges(int yr);
void initTimeChanges();
time_t toTime_t(TimeChangeRule r, int yr);
TimeChangeRule m_dst; // rule for start of dst or summer time for any year
TimeChangeRule m_std; // rule for start of standard time for any year
time_t m_dstUTC; // dst start for given/current year, given in UTC
time_t m_stdUTC; // std time start for given/current year, given in UTC
time_t m_dstLoc; // dst start for given/current year, given in local time
time_t m_stdLoc; // std time start for given/current year, given in local time
};
#endif

View File

@@ -4,14 +4,18 @@ default_envs = dev
[env:dev]
platform = espressif8266
board = esp12e
framework = ${common.framework}
board_build.ldscript = eagle.flash.4m2m.ld
framework = arduino
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
lib_compat_mode = off
build_flags =
-D WEBSOCKET_DISABLED=1
-D DEBUG_MODE=1
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
monitor_speed = 115200 ; If serial port is shared with HAN, use 2400 and parity E (or N for Norwegian Kamstrup)
monitor_speed = 115200 ; If serial port is shared with HAN, use baud and parity configured for meter
monitor_flags =
--parity
N

View File

@@ -2,31 +2,47 @@
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_deps = Timezone@1.2.4, 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.1
lib_ignore = OneWire
[env:esp8266]
platform = espressif8266@3.2.0
framework = arduino
board = esp12e
board_build.ldscript = eagle.flash.4m2m.ld
framework = arduino
build_flags = -D WEBSOCKET_DISABLED=1
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
[env:esp32]
platform = https://github.com/platformio/platform-espressif32.git#feature/arduino-upstream
board = esp32dev
framework = arduino
board = esp32dev
board_build.f_cpu = 160000000L
build_flags = -D WEBSOCKET_DISABLED=1
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
[env:esp32s2]
platform = https://github.com/tasmota/platform-espressif32.git#v2.0.3
platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.2
framework = arduino
board = esp32dev
board_build.mcu = esp32s2
board_build.variant = esp32s2
board_build.flash_mode = qio
board_build.f_cpu = 160000000L
board_build.f_flash = 40000000L
build_flags = -D WEBSOCKET_DISABLED=1
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

@@ -1,16 +1,24 @@
import os
import subprocess
from time import time
FILENAME_VERSION_H = 'src/version.h'
version = os.environ.get('GITHUB_TAG')
if version == None:
version = "SNAPSHOT"
import datetime
try:
result = subprocess.run(['git','rev-parse','--short','HEAD'], capture_output=True, check=False)
if result.returncode == 0:
version = result.stdout.decode('utf-8').strip()
else:
version = "SNAPSHOT"
except:
version = "SNAPSHOT"
hf = """
#ifndef VERSION
#define VERSION "{}"
#endif
""".format(version)
#define BUILD_EPOCH {}
""".format(version, time())
with open(FILENAME_VERSION_H, 'w+') as f:
f.write(hf)

40
scripts/esp32/flash.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/bin/sh
if [ -z "$1" ];then
echo "Usage: "
echo " Flashing first time : $0 flash /dev/ttyUSB0"
echo " When upgrading to new version : $0 upgrade /dev/ttyUSB0"
echo " NOTE: Replace /dev/ttyUSB0 with correct port"
exit 1
fi
if [ -z "$2" ];then
echo "Please specify port"
exit 1
fi
esptool=`which esptool`
if [ -z "$esptool" ];then
esptool=`which esptool.py`
fi
if [ -z "$esptool" ];then
if [ -f esptool.py ];then
esptool="esptool.py"
fi
fi
if [ -z "$esptool" ];then
echo "esptool.py not available to run following command: "
esptool="echo esptool.py"
fi
if [ "$1" = "flash" ];then
$esptool --chip esp32 --port $2 --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect \
0x1000 bootloader_dio_40m.bin \
0x8000 partitions.bin \
0xe000 boot_app0.bin \
0x10000 firmware.bin
exit $?
elif [ "$1" = "upgrade" ];then
$esptool --chip esp32 --port $2 --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x10000 firmware.bin
exit $?
fi

14
scripts/esp32/mkzip.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
env="esp32"
build_dir=".pio/build/$env/"
if [ ! -d $build_dir ];then
echo "No build directory"
exit 1
fi
cp ~/.platformio/packages/framework-arduinoespressif32/tools/sdk/$env/bin/bootloader_dio_40m.bin $build_dir
cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin $build_dir
chmod +x scripts/$env/flash.sh
zip -j $env.zip $build_dir/*.bin scripts/$env/flash.sh

40
scripts/esp32s2/flash.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/bin/sh
if [ -z "$1" ];then
echo "Usage: "
echo " Flashing first time : $0 flash /dev/ttyUSB0"
echo " When upgrading to new version : $0 upgrade /dev/ttyUSB0"
echo " NOTE: Replace /dev/ttyUSB0 with correct port"
exit 1
fi
if [ -z "$2" ];then
echo "Please specify port"
exit 1
fi
esptool=`which esptool`
if [ -z "$esptool" ];then
esptool=`which esptool.py`
fi
if [ -z "$esptool" ];then
if [ -f esptool.py ];then
esptool="esptool.py"
fi
fi
if [ -z "$esptool" ];then
echo "esptool.py not available to run following command: "
esptool="echo esptool.py"
fi
if [ "$1" = "flash" ];then
$esptool --chip esp32s2 --port $2 --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect \
0x1000 bootloader_qio_40m.bin \
0x8000 partitions.bin \
0xe000 boot_app0.bin \
0x10000 firmware.bin
exit $?
elif [ "$1" = "upgrade" ];then
$esptool --chip esp32s2 --port $2 --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x10000 firmware.bin
exit $?
fi

14
scripts/esp32s2/mkzip.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
env="esp32s2"
build_dir=".pio/build/$env/"
if [ ! -d $build_dir ];then
echo "No build directory"
exit 1
fi
cp ~/.platformio/packages/framework-arduinoespressif32/tools/sdk/$env/bin/bootloader_qio_40m.bin $build_dir
cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin $build_dir
chmod +x scripts/$env/flash.sh
zip -j $env.zip $build_dir/*.bin scripts/$env/flash.sh

31
scripts/esp8266/flash.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/bin/sh
if [ -z "$1" ];then
echo "Usage: "
echo " Flashing first time : $0 flash /dev/ttyUSB0"
echo " When upgrading to new version : $0 upgrade /dev/ttyUSB0"
echo " NOTE: Replace /dev/ttyUSB0 with correct port"
exit 1
fi
if [ -z "$2" ];then
echo "Please specify port"
exit 1
fi
esptool=`which esptool`
if [ -z "$esptool" ];then
esptool=`which esptool.py`
fi
if [ -z "$esptool" ];then
if [ -f esptool.py ];then
esptool="esptool.py"
fi
fi
if [ -z "$esptool" ];then
echo "esptool.py not available to run following command: "
esptool="echo esptool.py"
fi
$esptool --before default_reset --after hard_reset --chip esp8266 --port $2 --baud 115200 write_flash 0x0 firmware.bin
exit $?

12
scripts/esp8266/mkzip.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
env="esp8266"
build_dir=".pio/build/$env/"
if [ ! -d $build_dir ];then
echo "No build directory"
exit 1
fi
chmod +x scripts/$env/flash.sh
zip -j $env.zip $build_dir/*.bin scripts/$env/flash.sh

View File

@@ -1,6 +1,7 @@
import os
import re
import shutil
import subprocess
try:
from css_html_js_minify import html_minify, js_minify, css_minify
@@ -29,7 +30,14 @@ srcroot = "src/web/root"
version = os.environ.get('GITHUB_TAG')
if version == None:
version = "SNAPSHOT"
try:
result = subprocess.run(['git','rev-parse','--short','HEAD'], capture_output=True, check=False)
if result.returncode == 0:
version = result.stdout.decode('utf-8').strip()
else:
version = "SNAPSHOT"
except:
version = "SNAPSHOT"
if os.path.exists(srcroot):
shutil.rmtree(srcroot)
@@ -59,7 +67,7 @@ for filename in os.listdir(webroot):
print("WARN: Unable to minify")
with open(dstfile, "w") as dst:
dst.write("const char ")
dst.write("static const char ")
dst.write(varname)
dst.write("[] PROGMEM = R\"==\"==(")
dst.write(content)

11
scripts/mkzip.sh Normal file
View File

@@ -0,0 +1,11 @@
#!/bin/sh
find -name firmware.bin | while read firmware;do
dir=`dirname $firmware`
env=`basename $dir`
if [ -f scripts/$env/mkzip.sh ];then
echo "Building zip for env '$env'"
chmod +x scripts/$env/mkzip.sh
scripts/$env/mkzip.sh
fi
done

View File

@@ -37,11 +37,14 @@ bool AmsConfiguration::setWiFiConfig(WiFiConfig& config) {
wifiChanged |= strcmp(config.ssid, existing.ssid) != 0;
wifiChanged |= strcmp(config.psk, existing.psk) != 0;
wifiChanged |= strcmp(config.ip, existing.ip) != 0;
wifiChanged |= strcmp(config.gateway, existing.gateway) != 0;
wifiChanged |= strcmp(config.subnet, existing.subnet) != 0;
wifiChanged |= strcmp(config.dns1, existing.dns1) != 0;
wifiChanged |= strcmp(config.dns2, existing.dns2) != 0;
if(strlen(config.ip) > 0) {
wifiChanged |= strcmp(config.gateway, existing.gateway) != 0;
wifiChanged |= strcmp(config.subnet, existing.subnet) != 0;
wifiChanged |= strcmp(config.dns1, existing.dns1) != 0;
wifiChanged |= strcmp(config.dns2, existing.dns2) != 0;
}
wifiChanged |= strcmp(config.hostname, existing.hostname) != 0;
wifiChanged |= config.power != existing.power;
} else {
wifiChanged = true;
}
@@ -209,6 +212,10 @@ void AmsConfiguration::clearMeter(MeterConfig& config) {
config.productionCapacity = 0;
memset(config.encryptionKey, 0, 16);
memset(config.authenticationKey, 0, 16);
config.wattageMultiplier = 0;
config.voltageMultiplier = 0;
config.amperageMultiplier = 0;
config.accumulatedMultiplier = 0;
}
bool AmsConfiguration::isMeterChanged() {
@@ -232,6 +239,8 @@ bool AmsConfiguration::getDebugConfig(DebugConfig& config) {
}
bool AmsConfiguration::setDebugConfig(DebugConfig& config) {
if(!config.serial && !config.telnet)
config.level = 4; // Force warning level when debug is disabled
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_DEBUG_START, config);
bool ret = EEPROM.commit();
@@ -449,6 +458,9 @@ bool AmsConfiguration::getEntsoeConfig(EntsoeConfig& config) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_ENTSOE_START, config);
EEPROM.end();
if(strlen(config.token) != 0 && strlen(config.token) != 36) {
clearEntsoe(config);
}
return true;
} else {
return false;
@@ -487,6 +499,63 @@ void AmsConfiguration::ackEntsoeChange() {
entsoeChanged = false;
}
bool AmsConfiguration::getEnergyAccountingConfig(EnergyAccountingConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_ENERGYACCOUNTING_START, config);
EEPROM.end();
if(config.thresholds[9] != 255) {
clearEnergyAccountingConfig(config);
}
return true;
} else {
return false;
}
}
bool AmsConfiguration::setEnergyAccountingConfig(EnergyAccountingConfig& config) {
EnergyAccountingConfig existing;
if(getEnergyAccountingConfig(existing)) {
for(int i = 0; i < 9; i++) {
if(existing.thresholds[i] != config.thresholds[i]) {
energyAccountingChanged = true;
}
}
config.thresholds[9] = 255;
} else {
energyAccountingChanged = true;
}
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_ENERGYACCOUNTING_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
void AmsConfiguration::clearEnergyAccountingConfig(EnergyAccountingConfig& config) {
config.thresholds[0] = 5;
config.thresholds[1] = 10;
config.thresholds[2] = 15;
config.thresholds[3] = 20;
config.thresholds[4] = 25;
config.thresholds[5] = 50;
config.thresholds[6] = 75;
config.thresholds[7] = 100;
config.thresholds[8] = 150;
config.thresholds[9] = 255;
}
bool AmsConfiguration::isEnergyAccountingChanged() {
return energyAccountingChanged;
}
void AmsConfiguration::ackEnergyAccountingChange() {
energyAccountingChanged = false;
}
void AmsConfiguration::clear() {
EEPROM.begin(EEPROM_SIZE);
MeterConfig meter;
@@ -517,6 +586,10 @@ void AmsConfiguration::clear() {
clearEntsoe(entsoe);
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
EnergyAccountingConfig eac;
clearEnergyAccountingConfig(eac);
EEPROM.put(CONFIG_ENERGYACCOUNTING_START, eac);
EEPROM.put(EEPROM_CONFIG_ADDRESS, -1);
EEPROM.commit();
EEPROM.end();
@@ -528,62 +601,68 @@ bool AmsConfiguration::hasConfig() {
configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
EEPROM.end();
}
switch(configVersion) {
case 83:
configVersion = -1; // Prevent loop
if(loadConfig83(EEPROM_CONFIG_ADDRESS+1)) {
configVersion = EEPROM_CHECK_SUM;
return true;
} else {
configVersion = 0;
return false;
}
break;
case 86:
configVersion = -1; // Prevent loop
if(relocateConfig86()) {
configVersion = 87;
} else {
configVersion = 0;
return false;
}
case 87:
configVersion = -1; // Prevent loop
if(relocateConfig87()) {
configVersion = 88;
} else {
configVersion = 0;
return false;
}
case 88:
configVersion = -1; // Prevent loop
if(relocateConfig88()) {
configVersion = 89;
} else {
configVersion = 0;
return false;
}
case 89:
configVersion = -1; // Prevent loop
if(relocateConfig89()) {
configVersion = 90;
} else {
configVersion = 0;
return false;
}
case 90:
configVersion = -1; // Prevent loop
if(relocateConfig90()) {
configVersion = 91;
} else {
configVersion = 0;
return false;
}
case EEPROM_CHECK_SUM:
return true;
default:
if(configVersion > EEPROM_CHECK_SUM) {
if(loadFromFs(EEPROM_CHECK_SUM)) {
configVersion = EEPROM_CHECK_SUM;
} else {
configVersion = 0;
return false;
}
} else {
switch(configVersion) {
case 86:
configVersion = -1; // Prevent loop
if(relocateConfig86()) {
configVersion = 87;
} else {
configVersion = 0;
return false;
}
case 87:
configVersion = -1; // Prevent loop
if(relocateConfig87()) {
configVersion = 88;
} else {
configVersion = 0;
return false;
}
case 90:
configVersion = -1; // Prevent loop
if(relocateConfig90()) {
configVersion = 91;
} else {
configVersion = 0;
return false;
}
case 91:
configVersion = -1; // Prevent loop
if(relocateConfig91()) {
configVersion = 92;
} else {
configVersion = 0;
return false;
}
case 92:
configVersion = -1; // Prevent loop
if(relocateConfig92()) {
configVersion = 93;
} else {
configVersion = 0;
return false;
}
case 93:
configVersion = -1; // Prevent loop
if(relocateConfig93()) {
configVersion = 94;
} else {
configVersion = 0;
return false;
}
case EEPROM_CHECK_SUM:
return true;
default:
configVersion = 0;
return false;
}
}
return configVersion == EEPROM_CHECK_SUM;
}
@@ -630,116 +709,6 @@ void AmsConfiguration::saveTempSensors() {
}
}
bool AmsConfiguration::loadConfig83(int address) {
ConfigObject83 c;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(address, c);
EntsoeConfig entsoe {"", "", "", 1000};
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
NtpConfig ntp {
c.ntpEnable,
c.ntpDhcp,
c.ntpOffset,
c.ntpSummerOffset
};
strcpy(ntp.server, c.ntpServer);
EEPROM.put(CONFIG_NTP_START, ntp);
DomoticzConfig domo {
c.domoELIDX,
c.domoVL1IDX,
c.domoVL2IDX,
c.domoVL3IDX,
c.domoCL1IDX
};
EEPROM.put(CONFIG_DOMOTICZ_START, domo);
GpioConfig gpio {
c.hanPin,
c.apPin,
c.ledPin,
c.ledInverted,
c.ledPinRed,
c.ledPinGreen,
c.ledPinBlue,
c.ledRgbInverted,
c.tempSensorPin,
c.tempAnalogSensorPin,
c.vccPin,
c.vccOffset,
c.vccMultiplier,
c.vccBootLimit,
0,
0
};
EEPROM.put(CONFIG_GPIO_START, gpio);
DebugConfig debug {
c.debugTelnet,
c.debugSerial,
c.debugLevel
};
EEPROM.put(CONFIG_DEBUG_START, debug);
MeterConfig meter {
2400,
c.meterType == 3 || c.meterType == 4 ? 3 : 11,
false,
c.distributionSystem,
c.mainFuse,
c.productionCapacity,
{0},
{0}
};
memcpy(meter.encryptionKey, c.meterEncryptionKey, 16);
memcpy(meter.authenticationKey, c.meterAuthenticationKey, 16);
EEPROM.put(CONFIG_METER_START, meter);
WebConfig web {
c.authSecurity
};
strcpy(web.username, c.authUser);
strcpy(web.password, c.authPassword);
EEPROM.put(CONFIG_WEB_START, web);
MqttConfig mqtt;
strcpy(mqtt.host, c.mqttHost);
mqtt.port = c.mqttPort;
strcpy(mqtt.clientId, c.mqttClientId);
strcpy(mqtt.publishTopic, c.mqttPublishTopic);
strcpy(mqtt.subscribeTopic, c.mqttSubscribeTopic);
strcpy(mqtt.username, c.mqttUser);
strcpy(mqtt.password, c.mqttPassword);
mqtt.payloadFormat = c.mqttPayloadFormat;
mqtt.ssl = c.mqttSsl;
EEPROM.put(CONFIG_MQTT_START, mqtt);
WiFiConfig wifi;
strcpy(wifi.ssid, c.wifiSsid);
strcpy(wifi.psk, c.wifiPassword);
strcpy(wifi.ip, c.wifiIp);
strcpy(wifi.gateway, c.wifiGw);
strcpy(wifi.subnet, c.wifiSubnet);
strcpy(wifi.dns1, c.wifiDns1);
strcpy(wifi.dns2, c.wifiDns2);
strcpy(wifi.hostname, c.wifiHostname);
wifi.mdns = c.mDnsEnable;
EEPROM.put(CONFIG_WIFI_START, wifi);
SystemConfig sys {
c.boardType
};
EEPROM.put(CONFIG_SYSTEM_START, sys);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig86() {
MqttConfig86 mqtt86;
MqttConfig mqtt;
@@ -785,60 +754,6 @@ bool AmsConfiguration::relocateConfig87() {
return ret;
}
bool AmsConfiguration::relocateConfig88() {
GpioConfig88 gpio88;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_GPIO_START_88, gpio88);
GpioConfig gpio {
gpio88.hanPin,
gpio88.apPin,
gpio88.ledPin,
gpio88.ledInverted,
gpio88.ledPinRed,
gpio88.ledPinGreen,
gpio88.ledPinBlue,
gpio88.ledRgbInverted,
gpio88.tempSensorPin,
gpio88.tempAnalogSensorPin,
gpio88.vccPin,
gpio88.vccOffset,
gpio88.vccMultiplier,
gpio88.vccBootLimit,
0,
0
};
EEPROM.put(CONFIG_GPIO_START, gpio);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 89);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig89() {
EntsoeConfig89 entose89;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_ENTSOE_START_89, entose89);
uint32_t multiplier = entose89.multiplier;
EntsoeConfig entsoe = {
0x0,
0x0,
0x0,
multiplier
};
strcpy(entsoe.token, entose89.token);
strcpy(entsoe.area, entose89.area);
strcpy(entsoe.currency, entose89.currency);
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 90);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig90() {
EntsoeConfig entsoe;
EEPROM.begin(EEPROM_SIZE);
@@ -850,6 +765,63 @@ bool AmsConfiguration::relocateConfig90() {
return ret;
}
bool AmsConfiguration::relocateConfig91() {
WiFiConfig91 wifi91;
WiFiConfig wifi;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_WIFI_START_91, wifi91);
strcpy(wifi.ssid, wifi91.ssid);
strcpy(wifi.psk, wifi91.psk);
strcpy(wifi.ip, wifi91.ip);
strcpy(wifi.gateway, wifi91.gateway);
strcpy(wifi.subnet, wifi91.subnet);
strcpy(wifi.dns1, wifi91.dns1);
strcpy(wifi.dns2, wifi91.dns2);
strcpy(wifi.hostname, wifi91.hostname);
wifi.mdns = wifi91.mdns;
EEPROM.put(CONFIG_WIFI_START, wifi);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 92);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig92() {
WiFiConfig wifi;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_WIFI_START, wifi);
#if defined(ESP32)
wifi.power = 195;
#elif defined(ESP8266)
wifi.power = 205;
#endif
EEPROM.put(CONFIG_WIFI_START, wifi);
EnergyAccountingConfig eac;
clearEnergyAccountingConfig(eac);
EEPROM.put(CONFIG_ENERGYACCOUNTING_START, eac);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 93);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig93() {
MeterConfig meter;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_METER_START_93, meter);
meter.wattageMultiplier = 0;
meter.voltageMultiplier = 0;
meter.amperageMultiplier = 0;
meter.accumulatedMultiplier = 0;
EEPROM.put(CONFIG_METER_START, meter);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 94);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::save() {
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM);
@@ -866,6 +838,8 @@ uint8_t AmsConfiguration::getTempSensorCount() {
}
TempSensorConfig* AmsConfiguration::getTempSensorConfig(uint8_t address[8]) {
if(tempSensors == NULL)
return NULL;
for(int x = 0; x < tempSensorCount; x++) {
TempSensorConfig *conf = tempSensors[x];
if(isSensorAddressEqual(conf->address, address)) {
@@ -877,14 +851,16 @@ TempSensorConfig* AmsConfiguration::getTempSensorConfig(uint8_t address[8]) {
void AmsConfiguration::updateTempSensorConfig(uint8_t address[8], const char name[32], bool common) {
bool found = false;
for(int x = 0; x < tempSensorCount; x++) {
TempSensorConfig *data = tempSensors[x];
if(isSensorAddressEqual(data->address, address)) {
found = true;
strcpy(data->name, name);
data->common = common;
}
}
if(tempSensors != NULL) {
for(int x = 0; x < tempSensorCount; x++) {
TempSensorConfig *data = tempSensors[x];
if(isSensorAddressEqual(data->address, address)) {
found = true;
strcpy(data->name, name);
data->common = common;
}
}
}
if(!found) {
TempSensorConfig** tempSensors = new TempSensorConfig*[tempSensorCount+1];
if(this->tempSensors != NULL) {
@@ -911,37 +887,16 @@ bool AmsConfiguration::isSensorAddressEqual(uint8_t a[8], uint8_t b[8]) {
return true;
}
int AmsConfiguration::readString(int pAddress, char* pString[]) {
int address = 0;
byte length = EEPROM.read(pAddress + address);
address++;
char* buffer = new char[length];
for (int i = 0; i<length; i++)
{
buffer[i] = EEPROM.read(pAddress + address++);
}
*pString = buffer;
return address;
void AmsConfiguration::saveToFs() {
}
int AmsConfiguration::readInt(int address, int *value) {
int lower = EEPROM.read(address);
int higher = EEPROM.read(address + 1);
*value = lower + (higher << 8);
return 2;
bool AmsConfiguration::loadFromFs(uint8_t version) {
return false;
}
int AmsConfiguration::readBool(int address, bool *value) {
byte y = EEPROM.read(address);
*value = (bool)y;
return 1;
}
void AmsConfiguration::deleteFromFs(uint8_t version) {
int AmsConfiguration::readByte(int address, byte *value) {
*value = EEPROM.read(address);
return 1;
}
void AmsConfiguration::print(Print* debugger)

View File

@@ -4,15 +4,16 @@
#include "Arduino.h"
#define EEPROM_SIZE 1024*3
#define EEPROM_CHECK_SUM 91 // Used to check if config is stored. Change if structure changes
#define EEPROM_CHECK_SUM 94 // 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_METER_START 32
#define CONFIG_GPIO_START 266
#define CONFIG_ENTSOE_START 290
#define CONFIG_WIFI_START 360
#define CONFIG_ENERGYACCOUNTING_START 576
#define CONFIG_WEB_START 648
#define CONFIG_DEBUG_START 824
#define CONFIG_DOMOTICZ_START 856
@@ -21,16 +22,16 @@
#define CONFIG_MQTT_START_86 224
#define CONFIG_METER_START_87 784
#define CONFIG_GPIO_START_88 832
#define CONFIG_ENTSOE_START_89 944
#define CONFIG_ENTSOE_START_90 286
#define CONFIG_WIFI_START_91 16
#define CONFIG_METER_START_93 224
struct SystemConfig {
uint8_t boardType;
}; // 1
struct WiFiConfig {
struct WiFiConfig91 {
char ssid[32];
char psk[64];
char ip[15];
@@ -42,6 +43,19 @@ struct WiFiConfig {
bool mdns;
}; // 204
struct WiFiConfig {
char ssid[32];
char psk[64];
char ip[16];
char gateway[16];
char subnet[16];
char dns1[16];
char dns2[16];
char hostname[32];
bool mdns;
uint8_t power;
}; // 210
struct MqttConfig86 {
char host[128];
uint16_t port;
@@ -81,7 +95,11 @@ struct MeterConfig {
uint8_t productionCapacity;
uint8_t encryptionKey[16];
uint8_t authenticationKey[16];
}; // 41
uint16_t wattageMultiplier;
uint16_t voltageMultiplier;
uint16_t amperageMultiplier;
uint16_t accumulatedMultiplier;
}; // 49
struct MeterConfig87 {
uint8_t type;
@@ -118,23 +136,6 @@ struct GpioConfig {
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;
@@ -151,13 +152,6 @@ struct NtpConfig {
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];
@@ -165,71 +159,9 @@ struct EntsoeConfig {
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 EnergyAccountingConfig {
uint8_t thresholds[10];
}; // 10
struct TempSensorConfig {
uint8_t address[8];
@@ -301,6 +233,12 @@ public:
bool isEntsoeChanged();
void ackEntsoeChange();
bool getEnergyAccountingConfig(EnergyAccountingConfig&);
bool setEnergyAccountingConfig(EnergyAccountingConfig&);
void clearEnergyAccountingConfig(EnergyAccountingConfig&);
bool isEnergyAccountingChanged();
void ackEnergyAccountingChange();
void loadTempSensors();
void saveTempSensors();
uint8_t getTempSensorCount();
@@ -316,21 +254,20 @@ protected:
private:
uint8_t configVersion = 0;
bool wifiChanged, mqttChanged, meterChanged = true, domoChanged, ntpChanged = true, entsoeChanged = false;
bool wifiChanged, mqttChanged, meterChanged = true, domoChanged, ntpChanged = true, entsoeChanged = false, energyAccountingChanged = true;
uint8_t tempSensorCount = 0;
TempSensorConfig** tempSensors;
TempSensorConfig** tempSensors = NULL;
bool loadConfig83(int address);
bool relocateConfig86();
bool relocateConfig87();
bool relocateConfig88(); // dev 1.6
bool relocateConfig89(); // dev 1.6
bool relocateConfig86(); // 1.5.0
bool relocateConfig87(); // 1.5.4
bool relocateConfig90(); // 2.0.0
bool relocateConfig91(); // 2.0.2
bool relocateConfig92(); // 2.0.3
bool relocateConfig93(); // 2.1 beta
int readString(int pAddress, char* pString[]);
int readInt(int pAddress, int *pValue);
int readBool(int pAddress, bool *pValue);
int readByte(int pAddress, byte *pValue);
void saveToFs();
bool loadFromFs(uint8_t version);
void deleteFromFs(uint8_t version);
};
#endif

View File

@@ -7,22 +7,29 @@ void AmsData::apply(AmsData& other) {
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.0;
if(other.getActiveImportPower() > 0) {
float add = other.getActiveImportPower() * (((float) ms) / 3600000.0);
activeImportCounter += add / 1000.0;
//Serial.printf("%dW, %dms, %.6fkWh added\n", other.getActiveImportPower(), ms, add);
}
if(other.getListType() > 1) {
ms = this->lastUpdateMillis > other.getLastUpdateMillis() ? 0 : other.getLastUpdateMillis() - this->lastList2;
if(other.getActiveExportPower() > 0)
activeExportCounter += (((float) ms) * other.getActiveExportPower()) / 3600000000.0;
if(other.getReactiveImportPower() > 0)
reactiveImportCounter += (((float) ms) * other.getReactiveImportPower()) / 3600000000.0;
if(other.getReactiveExportPower() > 0)
reactiveExportCounter += (((float) ms) * other.getReactiveExportPower()) / 3600000000.0;
if(other.getActiveExportPower() > 0) {
float add = other.getActiveExportPower() * (((float) ms) / 3600000.0);
activeExportCounter += add / 1000.0;
}
if(other.getReactiveImportPower() > 0) {
float add = other.getReactiveImportPower() * (((float) ms) / 3600000.0);
reactiveImportCounter += add / 1000.0;
}
if(other.getReactiveExportPower() > 0) {
float add = other.getReactiveExportPower() * (((float) ms) / 3600000.0);
reactiveExportCounter += add / 1000.0;
}
}
counterEstimated = true;
}
} else {
//Serial.printf("\nDeviation: %.4f\n", other.getActiveImportCounter() - activeImportCounter);
}
this->lastUpdateMillis = other.getLastUpdateMillis();
@@ -33,11 +40,12 @@ void AmsData::apply(AmsData& other) {
if(other.getListType() > this->listType)
this->listType = other.getListType();
switch(other.getListType()) {
case 3:
case 4:
this->powerFactor = other.getPowerFactor();
this->l1PowerFactor = other.getL1PowerFactor();
this->l2PowerFactor = other.getL2PowerFactor();
this->l3PowerFactor = other.getL3PowerFactor();
case 3:
this->meterTimestamp = other.getMeterTimestamp();
this->activeImportCounter = other.getActiveImportCounter();
this->activeExportCounter = other.getActiveExportCounter();
@@ -153,19 +161,19 @@ float AmsData::getL3PowerFactor() {
return this->l3PowerFactor;
}
float AmsData::getActiveImportCounter() {
double AmsData::getActiveImportCounter() {
return this->activeImportCounter;
}
float AmsData::getReactiveImportCounter() {
double AmsData::getReactiveImportCounter() {
return this->reactiveImportCounter;
}
float AmsData::getActiveExportCounter() {
double AmsData::getActiveExportCounter() {
return this->activeExportCounter;
}
float AmsData::getReactiveExportCounter() {
double AmsData::getReactiveExportCounter() {
return this->reactiveExportCounter;
}

View File

@@ -53,10 +53,10 @@ public:
float getL2PowerFactor();
float getL3PowerFactor();
float getActiveImportCounter();
float getReactiveImportCounter();
float getActiveExportCounter();
float getReactiveExportCounter();
double getActiveImportCounter();
double getReactiveImportCounter();
double getActiveExportCounter();
double getReactiveExportCounter();
bool isThreePhase();
bool isTwoPhase();
@@ -71,7 +71,7 @@ protected:
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;
double activeImportCounter = 0, reactiveImportCounter = 0, activeExportCounter = 0, reactiveExportCounter = 0;
bool threePhase = false, twoPhase = false, counterEstimated = false;
};

View File

@@ -1,12 +1,12 @@
#include "AmsDataStorage.h"
#include <lwip/apps/sntp.h>
#include "EEPROM.h"
#include "LittleFS.h"
#include "AmsStorage.h"
#include "version.h"
AmsDataStorage::AmsDataStorage(RemoteDebug* debugger) {
day.version = 3;
month.version = 4;
day.version = 4;
month.version = 5;
this->debugger = debugger;
}
@@ -15,137 +15,160 @@ void AmsDataStorage::setTimezone(Timezone* 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(isHappy()) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Data is up to date\n");
return false;
}
if(now < EPOCH_2021_01_01) {
if(data->getMeterTimestamp() > 0) {
time_t now = time(nullptr);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Time is: %lld\n", (int64_t) now);
if(tz == NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Timezone is missing\n");
return false;
}
if(now < BUILD_EPOCH) {
if(data->getMeterTimestamp() > BUILD_EPOCH) {
now = data->getMeterTimestamp();
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Using meter timestamp, which is: %d\n", now);
debugger->printf("(AmsDataStorage) Using meter timestamp, which is: %lld\n", (int64_t) now);
}
} else if(data->getPackageTimestamp() > 0) {
} else if(data->getPackageTimestamp() > BUILD_EPOCH) {
now = data->getPackageTimestamp();
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Using package timestamp, which is: %d\n", now);
debugger->printf("(AmsDataStorage) Using package timestamp, which is: %lld\n", (int64_t) 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));
if(now < BUILD_EPOCH) {
if(debugger->isActive(RemoteDebug::VERBOSE)) {
debugger->printf("(AmsDataStorage) Invalid time: %lld\n", (int64_t) now);
}
return false;
}
tmElements_t tm, last;
breakTime(now, tm);
if(now > EPOCH_2021_01_01) {
tmElements_t last;
tmElements_t utc, ltz, utcYesterday, ltzYesterDay;
breakTime(now, utc);
breakTime(tz->toLocal(now), ltz);
breakTime(now-3600, utcYesterday);
breakTime(tz->toLocal(now-3600), ltzYesterDay);
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);
}
// Clear hours between last update and now
if(day.lastMeterReadTime > now) {
if(debugger->isActive(RemoteDebug::WARNING)) {
debugger->printf("(AmsDataStorage) Invalid future timestamp for day plot, resetting\n");
}
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);
day.activeImport = data->getActiveImportCounter() * 1000;
day.activeExport = data->getActiveExportCounter() * 1000;
day.lastMeterReadTime = now;
return true;
} else {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Last day update: %lld\n", (int64_t) day.lastMeterReadTime);
}
tmElements_t last;
breakTime(day.lastMeterReadTime, last);
for(int i = last.Hour; i < utc.Hour; i++) {
if(debugger->isActive(RemoteDebug::VERBOSE)) {
debugger->printf("(AmsDataStorage) Clearing hour: %d\n", i);
}
setHourImport(i, 0);
setHourExport(i, 0);
}
}
if(data->getListType() != 3) return false;
else if(tm.Minute > 5) return false;
// Clear days between last update and now
if(month.lastMeterReadTime > now) {
if(debugger->isActive(RemoteDebug::WARNING)) {
debugger->printf("(AmsDataStorage) Invalid future timestamp for month plot, resetting\n");
}
month.activeImport = data->getActiveImportCounter() * 1000;
month.activeExport = data->getActiveExportCounter() * 1000;
month.lastMeterReadTime = now;
} else {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Last month update: %lld\n", (int64_t) month.lastMeterReadTime);
}
tmElements_t last;
breakTime(tz->toLocal(month.lastMeterReadTime), last);
for(int i = last.Day; i < ltz.Day; i++) {
if(debugger->isActive(RemoteDebug::VERBOSE)) {
debugger->printf("(AmsDataStorage) Clearing day: %d\n", i);
}
setDayImport(i, 0);
setDayExport(i, 0);
}
}
if(data->getListType() < 3) {
debugger->printf("(AmsDataStorage) Not enough data in list type: %d\n", data->getListType());
return false;
}
bool ret = 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(!isDayHappy()) {
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++) {
setHourImport(i, 0);
setHourExport(i, 0);
}
} else if(now - day.lastMeterReadTime < 4000) {
uint32_t imp = (data->getActiveImportCounter() * 1000) - day.activeImport;
uint32_t exp = (data->getActiveExportCounter() * 1000) - day.activeExport;
setHourImport(utcYesterday.Hour, imp);
setHourExport(utcYesterday.Hour, exp);
if(debugger->isActive(RemoteDebug::INFO)) {
debugger->printf("(AmsDataStorage) Usage for hour %d: %d\n", tm.Hour, val);
}
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(AmsDataStorage) Usage for hour %d: %d - %d\n", ltzYesterDay.Hour, imp, exp);
day.activeImport = data->getActiveImportCounter() * 1000;
day.activeExport = data->getActiveExportCounter() * 1000;
day.lastMeterReadTime = now;
} else {
float mins = (now - day.lastMeterReadTime) / 60.0;
uint32_t im = (data->getActiveImportCounter() * 1000) - day.activeImport;
uint32_t ex = (data->getActiveExportCounter() * 1000) - day.activeExport;
float ipm = im / mins;
float epm = ex / mins;
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);
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);
}
day.activeImport += ipm * minutes;
day.activeExport += epm * minutes;
day.lastMeterReadTime = cur;
tmElements_t last;
breakTime(day.lastMeterReadTime, last);
day.lastMeterReadTime = day.lastMeterReadTime - (last.Minute * 60) - last.Second;
time_t stopAt = now - (utc.Minute * 60) - utc.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 imp = (ipm * minutes);
float exp = (epm * minutes);
setHourImport(last.Hour, imp);
setHourExport(last.Hour, exp);
if(debugger->isActive(RemoteDebug::INFO)) {
debugger->printf("(AmsDataStorage) Estimated usage for hour %u: %.1f - %.1f (%lld)\n", last.Hour, imp, exp, (int64_t) cur);
}
day.activeImport += imp;
day.activeExport += exp;
day.lastMeterReadTime = cur;
}
}
ret = true;
}
// 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(ltz.Hour == 0 && !isMonthHappy()) {
if(month.activeImport == 0 || now - month.lastMeterReadTime > 2678400) {
month.activeImport = data->getActiveImportCounter() * 1000;
month.activeExport = data->getActiveExportCounter() * 1000;
@@ -153,27 +176,35 @@ bool AmsDataStorage::update(AmsData* data) {
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);
for(int i = 1; i<=31; i++) {
setDayImport(i, 0);
setDayExport(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));
} else if(now - month.lastMeterReadTime < 86500 && now - month.lastMeterReadTime > 86300) {
int32_t imp = (data->getActiveImportCounter() * 1000) - month.activeImport;
int32_t exp = (data->getActiveExportCounter() * 1000) - month.activeExport;
if(debugger->isActive(RemoteDebug::INFO)) {
debugger->printf("(AmsDataStorage) Usage for day %d: %d\n", tm.Day, val);
debugger->printf("(AmsDataStorage) Usage for day %d: %d - %d\n", ltzYesterDay.Day, imp, exp);
}
time_t yesterday = now - 3600;
breakTime(yesterday, tm);
setDay(tm.Day, val);
setDayImport(ltzYesterDay.Day, imp);
setDayExport(ltzYesterDay.Day, exp);
month.activeImport = data->getActiveImportCounter() * 1000;
month.activeExport = data->getActiveExportCounter() * 1000;
month.lastMeterReadTime = now;
} else {
// Make sure last month read is at midnight
tmElements_t last;
breakTime(tz->toLocal(month.lastMeterReadTime), last);
month.lastMeterReadTime = month.lastMeterReadTime - (last.Hour * 3600) - (last.Minute * 60) - last.Second;
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Last month read after resetting to midnight: %lld\n", (int64_t) month.lastMeterReadTime);
}
float hrs = (now - month.lastMeterReadTime) / 3600.0;
uint16_t im = ((data->getActiveImportCounter() * 1000) - month.activeImport);
uint16_t ex = ((data->getActiveExportCounter() * 1000) - month.activeExport);
uint32_t im = (data->getActiveImportCounter() * 1000) - month.activeImport;
uint32_t ex = (data->getActiveExportCounter() * 1000) - month.activeExport;
float iph = im / hrs;
float eph = ex / hrs;
@@ -181,63 +212,69 @@ bool AmsDataStorage::update(AmsData* data) {
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;
time_t stopAt = now - (ltz.Hour * 3600) - (ltz.Minute * 60) - ltz.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);
breakTime(tz->toLocal(month.lastMeterReadTime), last);
float imp = (iph * hours);
float exp = (eph * hours);
setDayImport(last.Day, imp);
setDayExport(last.Day, exp);
if(debugger->isActive(RemoteDebug::INFO)) {
debugger->printf("(AmsDataStorage) Estimated usage for day %u: %.1f (%lu)\n", last.Day, val, cur);
debugger->printf("(AmsDataStorage) Estimated usage for day %u: %.1f - %.1f (%lld)\n", last.Day, imp, exp, (int64_t) cur);
}
month.activeImport += iph * hours;
month.activeExport += eph * hours;
month.lastMeterReadTime += cur;
month.activeImport += imp;
month.activeExport += exp;
month.lastMeterReadTime = cur;
}
}
ret = true;
}
return true;
return ret;
}
void AmsDataStorage::setHour(uint8_t hour, int32_t val) {
if(hour < 0) return;
day.points[hour] = val / 10;
void AmsDataStorage::setHourImport(uint8_t hour, int32_t val) {
if(hour < 0 || hour > 24) return;
day.hImport[hour] = val / 10;
}
int16_t AmsDataStorage::getHour(uint8_t hour) {
if(hour < 0) return 0;
return day.points[hour] * 10;
int32_t AmsDataStorage::getHourImport(uint8_t hour) {
if(hour < 0 || hour > 24) return 0;
return day.hImport[hour] * 10;
}
void AmsDataStorage::setDay(uint8_t day, int32_t val) {
if(day < 1) return;
month.points[day-1] = val / 10;
void AmsDataStorage::setHourExport(uint8_t hour, int32_t val) {
if(hour < 0 || hour > 24) return;
day.hExport[hour] = val / 10;
}
int32_t AmsDataStorage::getDay(uint8_t day) {
if(day < 1) return 0;
return (month.points[day-1] * 10);
int32_t AmsDataStorage::getHourExport(uint8_t hour) {
if(hour < 0 || hour > 24) return 0;
return day.hExport[hour] * 10;
}
void AmsDataStorage::setDayImport(uint8_t day, int32_t val) {
if(day < 1 || day > 31) return;
month.dImport[day-1] = val / 10;
}
int32_t AmsDataStorage::getDayImport(uint8_t day) {
if(day < 1 || day > 31) return 0;
return (month.dImport[day-1] * 10);
}
void AmsDataStorage::setDayExport(uint8_t day, int32_t val) {
if(day < 1 || day > 31) return;
month.dExport[day-1] = val / 10;
}
int32_t AmsDataStorage::getDayExport(uint8_t day) {
if(day < 1 || day > 31) return 0;
return (month.dExport[day-1] * 10);
}
bool AmsDataStorage::load() {
@@ -255,13 +292,7 @@ bool AmsDataStorage::load() {
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;
}
ret = setDayData(*day);
}
if(LittleFS.exists(FILE_MONTHPLOT)) {
@@ -270,13 +301,7 @@ bool AmsDataStorage::load() {
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;
}
ret = ret && setMonthData(*month);
}
LittleFS.end();
@@ -295,7 +320,7 @@ bool AmsDataStorage::save() {
File file = LittleFS.open(FILE_DAYPLOT, "w");
char buf[sizeof(day)];
memcpy(buf, &day, sizeof(day));
for(int i = 0; i < sizeof(day); i++) {
for(unsigned long i = 0; i < sizeof(day); i++) {
file.write(buf[i]);
}
file.close();
@@ -304,7 +329,7 @@ bool AmsDataStorage::save() {
File file = LittleFS.open(FILE_MONTHPLOT, "w");
char buf[sizeof(month)];
memcpy(buf, &month, sizeof(month));
for(int i = 0; i < sizeof(month); i++) {
for(unsigned long i = 0; i < sizeof(month); i++) {
file.write(buf[i]);
}
file.close();
@@ -313,3 +338,92 @@ bool AmsDataStorage::save() {
LittleFS.end();
return true;
}
DayDataPoints AmsDataStorage::getDayData() {
return day;
}
MonthDataPoints AmsDataStorage::getMonthData() {
return month;
}
bool AmsDataStorage::setDayData(DayDataPoints& day) {
if(day.version == 4) {
this->day = day;
return true;
} else if(day.version == 3) {
this->day = day;
for(uint8_t i = 0; i < 24; i++) this->day.hExport[i] = 0;
this->day.version = 4;
return true;
}
return false;
}
bool AmsDataStorage::setMonthData(MonthDataPoints& month) {
if(month.version == 5) {
this->month = month;
return true;
} else if(month.version == 4) {
this->month = month;
for(uint8_t i = 0; i < 31; i++) this->month.dExport[i] = 0;
this->month.version = 5;
return true;
}
return false;
}
bool AmsDataStorage::isHappy() {
return isDayHappy() && isMonthHappy();
}
bool AmsDataStorage::isDayHappy() {
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return false;
tmElements_t tm, last;
if(now < day.lastMeterReadTime) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(AmsDataStorage) Day %lld < %lld\n", (int64_t) now, (int64_t) day.lastMeterReadTime);
return false;
}
if(now-day.lastMeterReadTime > 3600) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(AmsDataStorage) Day %lld - %lld > 3600\n", (int64_t) now, (int64_t) day.lastMeterReadTime);
return false;
}
breakTime(tz->toLocal(now), tm);
breakTime(tz->toLocal(day.lastMeterReadTime), last);
if(tm.Hour > last.Hour) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(AmsDataStorage) Day %d > %d\n", tm.Hour, last.Hour);
return false;
}
return true;
}
bool AmsDataStorage::isMonthHappy() {
if(tz == NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Timezone is missing\n");
return false;
}
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return false;
tmElements_t tm, last;
if(now < month.lastMeterReadTime) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(AmsDataStorage) Month %lld < %lld\n", (int64_t) now, (int64_t) month.lastMeterReadTime);
return false;
}
if(now-month.lastMeterReadTime > 86400) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(AmsDataStorage) Month %lld - %lld > 3600\n", (int64_t) now, (int64_t) month.lastMeterReadTime);
return false;
}
breakTime(tz->toLocal(now), tm);
breakTime(tz->toLocal(month.lastMeterReadTime), last);
if(tm.Day > last.Day) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(AmsDataStorage) Month %d > %d\n", tm.Day, last.Day);
return false;
}
return true;
}

View File

@@ -5,45 +5,64 @@
#include "RemoteDebug.h"
#include "Timezone.h"
#define EPOCH_2021_01_01 1609459200
struct DayDataPoints {
uint8_t version;
int16_t points[24];
int16_t hImport[24];
time_t lastMeterReadTime;
uint32_t activeImport;
uint32_t activeExport;
}; // 37 bytes
int16_t hExport[24];
}; // 112 bytes
struct MonthDataPoints {
uint8_t version;
int16_t points[31];
int16_t dImport[31];
time_t lastMeterReadTime;
uint32_t activeImport;
uint32_t activeExport;
}; // 75 bytes
int16_t dExport[31];
}; // 141 bytes
class AmsDataStorage {
public:
AmsDataStorage(RemoteDebug*);
void setTimezone(Timezone*);
bool update(AmsData*);
int16_t getHour(uint8_t);
int32_t getDay(uint8_t);
int32_t getHourImport(uint8_t);
int32_t getHourExport(uint8_t);
int32_t getDayImport(uint8_t);
int32_t getDayExport(uint8_t);
bool load();
bool save();
DayDataPoints getDayData();
bool setDayData(DayDataPoints&);
MonthDataPoints getMonthData();
bool setMonthData(MonthDataPoints&);
bool isHappy();
bool isDayHappy();
bool isMonthHappy();
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
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, 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
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, 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);
void setHourImport(uint8_t, int32_t);
void setHourExport(uint8_t, int32_t);
void setDayImport(uint8_t, int32_t);
void setDayExport(uint8_t, int32_t);
};
#endif

View File

@@ -9,5 +9,8 @@
#define FILE_DAYPLOT "/dayplot.bin"
#define FILE_MONTHPLOT "/monthplot.bin"
#define FILE_ENERGYACCOUNTING "/energyaccounting.bin"
#define FILE_CFG "/configfile.cfg"
#endif

View File

@@ -1,7 +1,7 @@
#ifndef _AMSTOMQTTBRIDGE_H
#define _AMSTOMQTTBRIDGE_H
#define WIFI_CONNECTION_TIMEOUT 25000;
#define WIFI_CONNECTION_TIMEOUT 30000;
#define INVALID_BUTTON_PIN 0xFFFFFFFF

File diff suppressed because it is too large Load Diff

265
src/EnergyAccounting.cpp Normal file
View File

@@ -0,0 +1,265 @@
#include "EnergyAccounting.h"
#include "LittleFS.h"
#include "AmsStorage.h"
#include "version.h"
EnergyAccounting::EnergyAccounting(RemoteDebug* debugger) {
data.version = 1;
this->debugger = debugger;
}
void EnergyAccounting::setup(AmsDataStorage *ds, EntsoeApi *eapi, EnergyAccountingConfig *config) {
this->ds = ds;
this->eapi = eapi;
this->config = config;
this->currentThresholdIdx = 0;
}
EnergyAccountingConfig* EnergyAccounting::getConfig() {
return config;
}
void EnergyAccounting::setTimezone(Timezone* tz) {
this->tz = tz;
}
bool EnergyAccounting::update(AmsData* amsData) {
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return false;
if(tz == NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) Timezone is missing\n");
return false;
}
bool ret = false;
tmElements_t local;
breakTime(tz->toLocal(now), local);
if(!init) {
currentHour = local.Hour;
currentDay = local.Day;
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) Initializing data at %lld\n", (int64_t) now);
if(!load()) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) Unable to load existing data");
data = { 1, local.Month, 0, 0, 0, 0 };
if(calcDayUse()) ret = true;
}
init = true;
}
if(!initPrice && eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) Initializing prices at %lld\n", (int64_t) now);
calcDayCost();
}
if(local.Hour != currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New local hour %d\n", local.Hour);
if(calcDayUse()) ret = true;
if(local.Hour > 0) {
calcDayCost();
}
use = 0;
costHour = 0;
currentHour = local.Hour;
if(local.Day != currentDay) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New day %d\n", local.Day);
data.costYesterday = costDay * 100;
data.costThisMonth += costDay * 100;
costDay = 0;
currentDay = local.Day;
ret = true;
}
if(local.Month != data.month) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New month %d\n", local.Month);
data.costLastMonth = data.costThisMonth;
data.costThisMonth = 0;
data.maxHour = 0;
data.month = local.Month;
currentThresholdIdx = 0;
ret = true;
}
}
unsigned long ms = this->lastUpdateMillis > amsData->getLastUpdateMillis() ? 0 : amsData->getLastUpdateMillis() - this->lastUpdateMillis;
float kwh = (amsData->getActiveImportPower() * (((float) ms) / 3600000.0)) / 1000.0;
lastUpdateMillis = amsData->getLastUpdateMillis();
if(kwh > 0) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) Adding %.4f kWh\n", kwh);
use += kwh;
if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
float price = eapi->getValueForHour(0);
float cost = price * kwh;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) and %.4f %s\n", cost / 100.0, eapi->getCurrency());
costHour += cost;
costDay += cost;
}
}
if(config != NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) calculating threshold, currently at %d\n", currentThresholdIdx);
while(getMonthMax() > config->thresholds[currentThresholdIdx] && currentThresholdIdx < 10) currentThresholdIdx++;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) new threshold %d\n", currentThresholdIdx);
}
return ret;
}
bool EnergyAccounting::calcDayUse() {
time_t now = time(nullptr);
tmElements_t local, utc;
breakTime(tz->toLocal(now), local);
bool ret = false;
uint8_t lim = local.Day == 1 ? local.Hour : 24;
for(int i = 0; i < lim; i++) {
breakTime(now - ((lim - i) * 3600), utc);
int16_t val = ds->getHourImport(utc.Hour) / 10.0;
if(val > data.maxHour) {
data.maxHour = val;
ret = true;
}
}
return ret;
}
void EnergyAccounting::calcDayCost() {
time_t now = time(nullptr);
tmElements_t local, utc;
breakTime(tz->toLocal(now), local);
if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
if(initPrice) costDay = 0;
for(int i = 0; i < local.Hour; i++) {
float price = eapi->getValueForHour(i - local.Hour);
if(price == ENTSOE_NO_VALUE) break;
breakTime(now - ((local.Hour - i) * 3600), utc);
int16_t wh = ds->getHourImport(utc.Hour);
costDay += price * (wh / 1000.0);
}
initPrice = true;
}
}
double EnergyAccounting::getUseThisHour() {
return use;
}
double EnergyAccounting::getCostThisHour() {
return costHour;
}
double EnergyAccounting::getUseToday() {
float ret = 0.0;
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return 0;
tmElements_t local, utc;
breakTime(tz->toLocal(now), local);
for(int i = 0; i < local.Hour; i++) {
breakTime(now - ((local.Hour - i) * 3600), utc);
ret += ds->getHourImport(utc.Hour) / 1000.0;
}
return ret + getUseThisHour();
}
double EnergyAccounting::getCostToday() {
return costDay;
}
double EnergyAccounting::getCostYesterday() {
return data.costYesterday / 100.0;
}
double EnergyAccounting::getUseThisMonth() {
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return 0;
tmElements_t tm;
if(tz != NULL)
breakTime(tz->toLocal(now), tm);
else
breakTime(now, tm);
float ret = 0;
for(int i = 0; i < tm.Day; i++) {
ret += ds->getDayImport(i) / 1000.0;
}
return ret + getUseToday();
}
double EnergyAccounting::getCostThisMonth() {
return (data.costThisMonth / 100.0) + getCostToday();
}
double EnergyAccounting::getCostLastMonth() {
return data.costLastMonth / 100.0;
}
uint8_t EnergyAccounting::getCurrentThreshold() {
if(config == NULL)
return 0;
return config->thresholds[currentThresholdIdx];
}
float EnergyAccounting::getMonthMax() {
return data.maxHour / 100.0;
}
bool EnergyAccounting::load() {
if(!LittleFS.begin()) {
if(debugger->isActive(RemoteDebug::ERROR)) {
debugger->printf("(EnergyAccounting) Unable to load LittleFS\n");
}
return false;
}
bool ret = false;
if(LittleFS.exists(FILE_ENERGYACCOUNTING)) {
File file = LittleFS.open(FILE_ENERGYACCOUNTING, "r");
char buf[file.size()];
file.readBytes(buf, file.size());
EnergyAccountingData* data = (EnergyAccountingData*) buf;
file.close();
if(data->version == 1) {
memcpy(&this->data, data, sizeof(this->data));
ret = true;
} else {
ret = false;
}
}
LittleFS.end();
return ret;
}
bool EnergyAccounting::save() {
if(!LittleFS.begin()) {
if(debugger->isActive(RemoteDebug::ERROR)) {
debugger->printf("(EnergyAccounting) Unable to load LittleFS\n");
}
return false;
}
{
File file = LittleFS.open(FILE_ENERGYACCOUNTING, "w");
char buf[sizeof(data)];
memcpy(buf, &data, sizeof(data));
for(unsigned long i = 0; i < sizeof(data); i++) {
file.write(buf[i]);
}
file.close();
}
LittleFS.end();
return true;
}
EnergyAccountingData EnergyAccounting::getData() {
return this->data;
}
void EnergyAccounting::setData(EnergyAccountingData& data) {
this->data = data;
}

59
src/EnergyAccounting.h Normal file
View File

@@ -0,0 +1,59 @@
#ifndef _ENERGYACCOUNTING_H
#define _ENERGYACCOUNTING_H
#include "Arduino.h"
#include "AmsData.h"
#include "AmsDataStorage.h"
#include "entsoe/EntsoeApi.h"
struct EnergyAccountingData {
uint8_t version;
uint8_t month;
uint16_t maxHour;
uint16_t costYesterday;
uint16_t costThisMonth;
uint16_t costLastMonth;
};
class EnergyAccounting {
public:
EnergyAccounting(RemoteDebug*);
void setup(AmsDataStorage *ds, EntsoeApi *eapi, EnergyAccountingConfig *config);
void setTimezone(Timezone*);
EnergyAccountingConfig* getConfig();
bool update(AmsData* amsData);
bool save();
double getUseThisHour();
double getCostThisHour();
double getUseToday();
double getCostToday();
double getCostYesterday();
double getUseThisMonth();
double getCostThisMonth();
double getCostLastMonth();
float getMonthMax();
uint8_t getCurrentThreshold();
EnergyAccountingData getData();
void setData(EnergyAccountingData&);
private:
RemoteDebug* debugger = NULL;
unsigned long lastUpdateMillis = 0;
bool init = false, initPrice = false;
AmsDataStorage *ds = NULL;
EntsoeApi *eapi = NULL;
EnergyAccountingConfig *config = NULL;
Timezone *tz = NULL;
uint8_t currentHour = 0, currentDay = 0, currentThresholdIdx = 0;
double use, costHour, costDay;
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0 };
bool load();
bool calcDayUse();
void calcDayCost();
};
#endif

View File

@@ -14,10 +14,14 @@ void HwTools::setup(GpioConfig* config, AmsConfiguration* amsConf) {
config->tempSensorPin = 0xFF;
}
#if defined(ESP32)
analogReadResolution(12);
analogRange = 4096;
#endif
if(config->vccPin > 0 && config->vccPin < 40) {
getAdcChannel(config->vccPin, voltAdc);
if(voltAdc.unit != 0xFF) {
#if defined(ESP32)
#if defined(CONFIG_IDF_TARGET_ESP32)
getAdcChannel(config->vccPin, voltAdc);
if(voltAdc.unit != 0xFF) {
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);
@@ -27,10 +31,10 @@ void HwTools::setup(GpioConfig* config, AmsConfiguration* amsConf) {
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 {
}
#else
pinMode(config->vccPin, INPUT);
}
#endif
} else {
voltAdc.unit = 0xFF;
voltAdc.channel = 0xFF;
@@ -156,13 +160,7 @@ void HwTools::getAdcChannel(uint8_t pin, AdcConfig& config) {
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 defined(CONFIG_IDF_TARGET_ESP32)
if(voltAdc.unit != 0xFF) {
uint32_t x = 0;
for (int i = 0; i < 10; i++) {
@@ -182,14 +180,22 @@ double HwTools::getVcc() {
for (int i = 0; i < 10; i++) {
x += analogRead(config->vccPin);
}
volts = x / 40950;
volts = (x * 3.3) / 10.0 / analogRange;
}
#else
uint32_t x = 0;
for (int i = 0; i < 10; i++) {
x += analogRead(config->vccPin);
}
volts = (x * 3.3) / 10.0 / analogRange;
#endif
} else {
#if defined(ESP8266)
volts = ESP.getVcc() / 1024.0;
#endif
}
if(volts == 0.0) return 0.0;
if(config->vccResistorGnd > 0 && config->vccResistorVcc > 0) {
volts *= ((double) (config->vccResistorGnd + config->vccResistorVcc) / config->vccResistorGnd);
}
@@ -303,12 +309,7 @@ double HwTools::getTemperature() {
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
int volts = ((double) analogRead(config->tempAnalogSensorPin) / analogRange) * 3.3;
return ((volts * adcCalibrationFactor) - 0.4) / 0.0195;
}
return DEVICE_DISCONNECTED_C;

View File

@@ -51,6 +51,7 @@ public:
HwTools() {};
private:
uint16_t analogRange = 1024;
AdcConfig voltAdc, tempAdc;
#if defined(ESP32)
esp_adc_cal_characteristics_t* voltAdcChar, tempAdcChar;

View File

@@ -1,13 +1,22 @@
#include "IEC6205621.h"
#include "ams/crc.h"
IEC6205621::IEC6205621(String payload) {
if(payload.length() < 16)
IEC6205621::IEC6205621(const char* p) {
if(strlen(p) < 16)
return;
String payload(p+1);
int crc_pos = payload.lastIndexOf("!");
String crc = payload.substring(crc_pos+1, crc_pos+5);
//uint16_t crc_calc = crc16_x25((uint8_t*) (payload.startsWith("/") ? p+1 : p), crc_pos);
//Serial.printf("CRC %s :: %04X\n", crc.c_str(), crc_calc);
lastUpdateMillis = millis();
listId = payload.substring(payload.startsWith("/") ? 1 : 0, payload.indexOf("\n"));
if(listId.startsWith("ADN")) {
meterType == AmsTypeAidon;
meterType = AmsTypeAidon;
listId = listId.substring(0,4);
} else if(listId.startsWith("KFM")) {
meterType = AmsTypeKaifa;
@@ -21,7 +30,7 @@ IEC6205621::IEC6205621(String payload) {
} else if(listId.startsWith("XMX")) {
meterType = AmsTypeLandis;
listId = listId.substring(0,6);
} else if(listId.startsWith("Ene")) {
} else if(listId.startsWith("Ene") || listId.startsWith("EST")) {
meterType = AmsTypeSagemcom;
listId = listId.substring(0,4);
} else {
@@ -55,10 +64,10 @@ IEC6205621::IEC6205621(String payload) {
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);
activeImportPower = (uint16_t) (extractDouble(payload, "1.7.0"));
activeExportPower = (uint16_t) (extractDouble(payload, "2.7.0"));
reactiveImportPower = (uint16_t) (extractDouble(payload, "3.7.0"));
reactiveExportPower = (uint16_t) (extractDouble(payload, "4.7.0"));
if(activeImportPower > 0)
listType = 1;
@@ -73,11 +82,40 @@ IEC6205621::IEC6205621(String payload) {
if(l1voltage > 0 || l2voltage > 0 || l3voltage > 0)
listType = 2;
double val = 0.0;
activeImportCounter = extractDouble(payload, "1.8.0");
activeExportCounter = extractDouble(payload, "2.8.0");
reactiveImportCounter = extractDouble(payload, "3.8.0");
reactiveExportCounter = extractDouble(payload, "4.8.0");
val = extractDouble(payload, "1.8.0");
if(val == 0) {
for(int i = 1; i < 9; i++) {
val += extractDouble(payload, "1.8." + String(i,10));
}
}
if(val > 0) activeImportCounter = val / 1000;
val = extractDouble(payload, "2.8.0");
if(val == 0) {
for(int i = 1; i < 9; i++) {
val += extractDouble(payload, "2.8." + String(i,10));
}
}
if(val > 0) activeExportCounter = val / 1000;
val = extractDouble(payload, "3.8.0");
if(val == 0) {
for(int i = 1; i < 9; i++) {
val += extractDouble(payload, "3.8." + String(i,10));
}
}
if(val > 0) reactiveImportCounter = val / 1000;
val = extractDouble(payload, "4.8.0");
if(val == 0) {
for(int i = 1; i < 9; i++) {
val += extractDouble(payload, "4.8." + String(i,10));
}
}
if(val > 0) reactiveExportCounter = val / 1000;
if(activeImportCounter > 0 || activeExportCounter > 0 || reactiveImportCounter > 0 || reactiveExportCounter > 0)
listType = 3;
@@ -104,5 +142,14 @@ String IEC6205621::extract(String payload, String obis) {
}
double IEC6205621::extractDouble(String payload, String obis) {
return extract(payload, obis).toDouble();
String str = extract(payload, obis);
if(str.isEmpty()) {
return 0.0;
}
int a = str.indexOf("*");
String val = str.substring(0,a);
String unit = str.substring(a+1);
return unit.startsWith("k") ? val.toDouble() * 1000 : val.toDouble();
}

View File

@@ -1,11 +1,12 @@
#ifndef _IEC62056_21_H
#define _IEC62056_21_H
#include "Arduino.h"
#include "AmsData.h"
class IEC6205621 : public AmsData {
public:
IEC6205621(String payload);
IEC6205621(const char* payload);
private:
String extract(String payload, String obis);

View File

@@ -2,19 +2,24 @@
#include "lwip/def.h"
#include "Timezone.h"
IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, CosemDateTime packageTimestamp) {
uint32_t ui;
IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterConfig, CosemDateTime packageTimestamp, HDLCConfig* hc) {
double val;
char str[64];
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
Timezone tz(CEST, CET);
this->packageTimestamp = getTimestamp(packageTimestamp);
ui = getNumber(AMS_OBIS_ACTIVE_IMPORT, sizeof(AMS_OBIS_ACTIVE_IMPORT), ((char *) (d)));
if(ui == 0xFFFFFFFF) {
val = getNumber(AMS_OBIS_ACTIVE_IMPORT, sizeof(AMS_OBIS_ACTIVE_IMPORT), ((char *) (d)));
if(val == NOVALUE) {
CosemData* data = getCosemDataAt(1, ((char *) (d)));
// Kaifa special case...
if(data->base.type == CosemTypeOctetString) {
this->packageTimestamp = this->packageTimestamp > 0 ? tz.toUTC(this->packageTimestamp) : 0;
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
String listId = String(str);
@@ -91,13 +96,19 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, CosemDateTime packag
l1voltage = ntohl(data->dlu.data) / 10.0;
}
if(listType >= 2 && memcmp(meterModel.c_str(), "MA304T3", 7) == 0) {
l2current = (((activeImportPower - activeExportPower) * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage;
l2voltage = sqrt(pow(l1voltage - l3voltage * cos(60 * (PI/180)), 2) + pow(l3voltage * sin(60 * (PI/180)),2));
}
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);
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) data;
time_t ts = getTimestamp(amst->dt);
meterTimestamp = tz.toUTC(ts);
}
}
}
@@ -116,6 +127,7 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, CosemDateTime packag
lastUpdateMillis = millis();
}
} else if(useMeterType == AmsTypeKaifa && data->base.type == CosemTypeDLongUnsigned) {
this->packageTimestamp = this->packageTimestamp > 0 ? tz.toUTC(this->packageTimestamp) : 0;
listType = 1;
meterType = AmsTypeKaifa;
activeImportPower = ntohl(data->dlu.data);
@@ -124,15 +136,17 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, CosemDateTime packag
// Kaifa end
} else {
listType = 1;
activeImportPower = ui;
activeImportPower = val;
meterType = AmsTypeUnknown;
CosemData* version = findObis(AMS_OBIS_VERSION, sizeof(AMS_OBIS_VERSION), d);
if(version != NULL && version->base.type == CosemTypeString) {
if(version != NULL && (version->base.type == CosemTypeString || version->base.type == CosemTypeOctetString)) {
if(memcmp(version->str.data, "AIDON", 5) == 0) {
meterType = AmsTypeAidon;
} else if(memcmp(version->str.data, "Kamstrup", 8) == 0) {
meterType = AmsTypeKamstrup;
} else if(memcmp(version->str.data, "KFM", 3) == 0) {
meterType = AmsTypeKaifa;
}
} else {
version = getCosemDataAt(1, ((char *) (d)));
@@ -140,106 +154,109 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, CosemDateTime packag
if(memcmp(version->str.data, "Kamstrup", 8) == 0) {
meterType = AmsTypeKamstrup;
}
}
}
// Try system title
if(meterType == AmsTypeUnknown && hc != NULL) {
if(memcmp(hc->system_title, "SAGY", 4) == 0) {
meterType = AmsTypeSagemcom;
}
}
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;
}
ui = getString(AMS_OBIS_VERSION, sizeof(AMS_OBIS_VERSION), ((char *) (d)), str);
if(ui > 0) {
uint8_t str_len = 0;
str_len = getString(AMS_OBIS_VERSION, sizeof(AMS_OBIS_VERSION), ((char *) (d)), str);
if(str_len > 0) {
listId = String(str);
}
ui = getNumber(AMS_OBIS_ACTIVE_EXPORT, sizeof(AMS_OBIS_ACTIVE_EXPORT), ((char *) (d)));
if(ui != 0xFFFFFFFF) {
activeExportPower = ui;
val = getNumber(AMS_OBIS_ACTIVE_EXPORT, sizeof(AMS_OBIS_ACTIVE_EXPORT), ((char *) (d)));
if(val != NOVALUE) {
activeExportPower = val;
}
ui = getNumber(AMS_OBIS_REACTIVE_IMPORT, sizeof(AMS_OBIS_REACTIVE_IMPORT), ((char *) (d)));
if(ui != 0xFFFFFFFF) {
reactiveImportPower = ui;
val = getNumber(AMS_OBIS_REACTIVE_IMPORT, sizeof(AMS_OBIS_REACTIVE_IMPORT), ((char *) (d)));
if(val != NOVALUE) {
reactiveImportPower = val;
}
ui = getNumber(AMS_OBIS_REACTIVE_EXPORT, sizeof(AMS_OBIS_REACTIVE_EXPORT), ((char *) (d)));
if(ui != 0xFFFFFFFF) {
reactiveExportPower = ui;
val = getNumber(AMS_OBIS_REACTIVE_EXPORT, sizeof(AMS_OBIS_REACTIVE_EXPORT), ((char *) (d)));
if(val != NOVALUE) {
reactiveExportPower = val;
}
val = getNumber(AMS_OBIS_VOLTAGE_L1, sizeof(AMS_OBIS_VOLTAGE_L1), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 2;
l1voltage = val;
}
val = getNumber(AMS_OBIS_VOLTAGE_L2, sizeof(AMS_OBIS_VOLTAGE_L2), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 2;
l2voltage = val;
}
val = getNumber(AMS_OBIS_VOLTAGE_L3, sizeof(AMS_OBIS_VOLTAGE_L3), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 2;
l3voltage = val;
}
val = getNumber(AMS_OBIS_CURRENT_L1, sizeof(AMS_OBIS_CURRENT_L1), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 2;
l1current = val;
}
val = getNumber(AMS_OBIS_CURRENT_L2, sizeof(AMS_OBIS_CURRENT_L2), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 2;
l2current = val;
}
val = getNumber(AMS_OBIS_CURRENT_L3, sizeof(AMS_OBIS_CURRENT_L3), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 2;
l3current = val;
}
val = getNumber(AMS_OBIS_ACTIVE_IMPORT_COUNT, sizeof(AMS_OBIS_ACTIVE_IMPORT_COUNT), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 3;
activeImportCounter = val / 1000.0;
}
val = getNumber(AMS_OBIS_ACTIVE_EXPORT_COUNT, sizeof(AMS_OBIS_ACTIVE_EXPORT_COUNT), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 3;
activeExportCounter = val / 1000.0;
}
val = getNumber(AMS_OBIS_REACTIVE_IMPORT_COUNT, sizeof(AMS_OBIS_REACTIVE_IMPORT_COUNT), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 3;
reactiveImportCounter = val / 1000.0;
}
val = getNumber(AMS_OBIS_REACTIVE_EXPORT_COUNT, sizeof(AMS_OBIS_REACTIVE_EXPORT_COUNT), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 3;
reactiveExportCounter = val / 1000.0;
}
ui = getString(AMS_OBIS_METER_MODEL, sizeof(AMS_OBIS_METER_MODEL), ((char *) (d)), str);
if(ui > 0) {
str_len = getString(AMS_OBIS_METER_MODEL, sizeof(AMS_OBIS_METER_MODEL), ((char *) (d)), str);
if(str_len > 0) {
meterModel = String(str);
} else {
ui = getString(AMS_OBIS_METER_MODEL_2, sizeof(AMS_OBIS_METER_MODEL_2), ((char *) (d)), str);
if(ui > 0) {
str_len = getString(AMS_OBIS_METER_MODEL_2, sizeof(AMS_OBIS_METER_MODEL_2), ((char *) (d)), str);
if(str_len > 0) {
meterModel = String(str);
}
}
ui = getString(AMS_OBIS_METER_ID, sizeof(AMS_OBIS_METER_ID), ((char *) (d)), str);
if(ui > 0) {
str_len = getString(AMS_OBIS_METER_ID, sizeof(AMS_OBIS_METER_ID), ((char *) (d)), str);
if(str_len > 0) {
meterId = String(str);
} else {
ui = getString(AMS_OBIS_METER_ID_2, sizeof(AMS_OBIS_METER_ID_2), ((char *) (d)), str);
if(ui > 0) {
str_len = getString(AMS_OBIS_METER_ID_2, sizeof(AMS_OBIS_METER_ID_2), ((char *) (d)), str);
if(str_len > 0) {
meterId = String(str);
}
}
@@ -249,52 +266,139 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, CosemDateTime packag
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs;
time_t ts = getTimestamp(amst->dt);
if(meterType == AmsTypeKamstrup || meterType == AmsTypeAidon) {
this->meterTimestamp = tz.toUTC(ts);
meterTimestamp = tz.toUTC(ts);
} else {
meterTimestamp = ts;
}
}
val = getNumber(AMS_OBIS_POWER_FACTOR, sizeof(AMS_OBIS_POWER_FACTOR), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 4;
powerFactor = val;
}
val = getNumber(AMS_OBIS_POWER_FACTOR_L1, sizeof(AMS_OBIS_POWER_FACTOR_L1), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 4;
l1PowerFactor = val;
}
val = getNumber(AMS_OBIS_POWER_FACTOR_L2, sizeof(AMS_OBIS_POWER_FACTOR_L2), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 4;
l2PowerFactor = val;
}
val = getNumber(AMS_OBIS_POWER_FACTOR_L3, sizeof(AMS_OBIS_POWER_FACTOR_L3), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 4;
l3PowerFactor = val;
}
if(meterType == AmsTypeKamstrup) {
activeImportCounter *= 10;
activeExportCounter *= 10;
reactiveImportCounter *= 10;
reactiveExportCounter *= 10;
l1current /= 100;
l2current /= 100;
l3current /= 100;
powerFactor /= 100;
l1PowerFactor /= 100;
l2PowerFactor /= 100;
l3PowerFactor /= 100;
if(listType >= 3) {
activeImportCounter *= 10;
activeExportCounter *= 10;
reactiveImportCounter *= 10;
reactiveExportCounter *= 10;
}
if(l1current != 0)
l1current /= 100;
if(l2current != 0)
l2current /= 100;
if(l3current != 0)
l3current /= 100;
if(powerFactor != 0)
powerFactor /= 100;
if(l1PowerFactor != 0)
l1PowerFactor /= 100;
if(l2PowerFactor != 0)
l2PowerFactor /= 100;
if(l3PowerFactor != 0)
l3PowerFactor /= 100;
int watt = abs((l1voltage * l1current) + (l2voltage * l2current) + (l3voltage * l3current));
if(watt / (activeImportPower + activeExportPower + reactiveImportPower + reactiveExportPower) > 2) {
l1current = l1current != 0 ? l1current / 10 : 0;
l2current = l2current != 0 ? l2current / 10 : 0;
l3current = l3current != 0 ? l3current / 10 : 0;
}
} else if(meterType == AmsTypeSagemcom) {
CosemData* meterTs = getCosemDataAt(1, ((char *) (d)));
if(meterTs != NULL) {
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs;
time_t ts = getTimestamp(amst->dt);
meterTimestamp = ts;
}
CosemData* mid = getCosemDataAt(58, ((char *) (d))); // TODO: Get last item
if(mid != NULL) {
switch(mid->base.type) {
case CosemTypeString:
memcpy(&meterId, mid->str.data, mid->str.length);
meterId[mid->str.length] = 0;
break;
case CosemTypeOctetString:
memcpy(&meterId, mid->oct.data, mid->oct.length);
meterId[mid->oct.length] = 0;
break;
}
}
} else if(meterType == AmsTypeKaifa) {
if(l1current != 0)
l1current /= 1000;
if(l2current != 0)
l2current /= 1000;
if(l3current != 0)
l3current /= 1000;
if(l1voltage != 0)
l1voltage /= 10;
if(l2voltage != 0)
l2voltage /= 10;
if(l3voltage != 0)
l3voltage /= 10;
}
lastUpdateMillis = millis();
}
threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0;
twoPhase = (l1voltage > 0 && l2voltage > 0) || (l2voltage > 0 && l3voltage > 0) || (l3voltage > 0 && l1voltage > 0);
if(meterConfig->wattageMultiplier > 0) {
activeImportPower = activeImportPower > 0 ? activeImportPower * (meterConfig->wattageMultiplier / 1000.0) : 0;
activeExportPower = activeExportPower > 0 ? activeExportPower * (meterConfig->wattageMultiplier / 1000.0) : 0;
reactiveImportPower = reactiveImportPower > 0 ? reactiveImportPower * (meterConfig->wattageMultiplier / 1000.0) : 0;
reactiveExportPower = reactiveExportPower > 0 ? reactiveExportPower * (meterConfig->wattageMultiplier / 1000.0) : 0;
}
if(meterConfig->voltageMultiplier > 0) {
l1voltage = l1voltage > 0 ? l1voltage * (meterConfig->voltageMultiplier / 1000.0) : 0;
l2voltage = l2voltage > 0 ? l2voltage * (meterConfig->voltageMultiplier / 1000.0) : 0;
l3voltage = l3voltage > 0 ? l3voltage * (meterConfig->voltageMultiplier / 1000.0) : 0;
}
if(meterConfig->amperageMultiplier > 0) {
l1current = l1current > 0 ? l1current * (meterConfig->amperageMultiplier / 1000.0) : 0;
l2current = l2current > 0 ? l2current * (meterConfig->amperageMultiplier / 1000.0) : 0;
l3current = l3current > 0 ? l3current * (meterConfig->amperageMultiplier / 1000.0) : 0;
}
if(meterConfig->accumulatedMultiplier > 0) {
activeImportCounter = activeImportCounter > 0 ? activeImportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
activeExportCounter = activeExportCounter > 0 ? activeExportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
reactiveImportCounter = reactiveImportCounter > 0 ? reactiveImportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
reactiveExportCounter = reactiveExportCounter > 0 ? reactiveExportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
}
if(threePhase) {
if(l2current == 0 && l1current > 0 && l3current > 0) {
l2current = (((activeImportPower - activeExportPower) * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage;
threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0;
if(!threePhase)
twoPhase = (l1voltage > 0 && l2voltage > 0) || (l2voltage > 0 && l3voltage > 0) || (l3voltage > 0 && l1voltage > 0);
// Special case for Norwegian IT/TT meters that does not report all values
if(meterConfig->distributionSystem == 1) {
if(threePhase) {
if(l2current == 0.0 && l1current > 0.0 && l3current > 0.0) {
l2current = (((activeImportPower - activeExportPower) * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage;
if(activeExportPower == 0.0) {
l2current = max((float) 0.0, l2current);
}
}
} else if(twoPhase && l1current > 0.0 && l2current > 0.0 && l3current > 0.0) {
l2voltage = sqrt(pow(l1voltage - l3voltage * cos(60.0 * (PI/180.0)), 2) + pow(l3voltage * sin(60.0 * (PI/180.0)),2));
threePhase = true;
}
}
}
@@ -316,20 +420,25 @@ CosemData* IEC6205675::getCosemDataAt(uint8_t index, const char* ptr) {
pos += 2 + item->base.length;
break;
case CosemTypeLongSigned:
pos += 5;
break;
case CosemTypeLongUnsigned:
pos += 3;
break;
case CosemTypeDLongSigned:
case CosemTypeDLongUnsigned:
pos += 5;
break;
case CosemTypeLong64Signed:
case CosemTypeLong64Unsigned:
pos += 9;
break;
case CosemTypeNull:
return NULL;
pos += 1;
break;
default:
pos += 2;
}
i++;
if(pos-ptr > 900) break;
} while(item->base.type != HDLC_FLAG);
return NULL;
}
@@ -359,19 +468,24 @@ CosemData* IEC6205675::findObis(uint8_t* obis, int matchlength, const char* ptr)
break;
}
case CosemTypeLongSigned:
pos += 5;
break;
case CosemTypeLongUnsigned:
pos += 3;
break;
case CosemTypeDLongSigned:
case CosemTypeDLongUnsigned:
pos += 5;
break;
case CosemTypeLong64Signed:
case CosemTypeLong64Unsigned:
pos += 9;
break;
case CosemTypeNull:
return NULL;
pos += 1;
break;
default:
pos += 2;
}
if(pos-ptr > 900) break;
} while(item->base.type != HDLC_FLAG);
return NULL;
}
@@ -399,29 +513,56 @@ double IEC6205675::getNumber(uint8_t* obis, int matchlength, const char* ptr) {
}
double IEC6205675::getNumber(CosemData* item) {
double val = 0xFFFFFFFF;
if(item != NULL) {
double ret = 0.0;
char* pos = ((char*) item);
switch(item->base.type) {
case CosemTypeLongUnsigned:
val = ntohs(item->lu.data);
case CosemTypeLongSigned: {
int16_t i16 = ntohs(item->ls.data);
ret = i16;
pos += 3;
break;
case CosemTypeDLongUnsigned:
val = ntohl(item->dlu.data);
}
case CosemTypeLongUnsigned: {
uint16_t u16 = ntohs(item->lu.data);
ret = u16;
pos += 3;
break;
}
case CosemTypeDLongSigned: {
int32_t i32 = ntohl(item->dlu.data);
ret = i32;
pos += 5;
break;
case CosemTypeLongSigned:
val = ntohs(item->lu.data);
pos += 3;
}
case CosemTypeDLongUnsigned: {
uint32_t u32 = ntohl(item->dlu.data);
ret = u32;
pos += 5;
break;
}
case CosemTypeLong64Signed: {
int64_t i64 = ntohll(item->l64s.data);
ret = i64;
pos += 9;
break;
}
case CosemTypeLong64Unsigned: {
uint64_t u64 = ntohll(item->l64u.data);
ret = u64;
pos += 9;
break;
}
}
if(*pos++ == 0x02 && *pos++ == 0x02) {
int8_t scale = *++pos;
val *= pow(10, scale);
if(pos != NULL) {
if(*pos++ == 0x02 && *pos++ == 0x02) {
int8_t scale = *++pos;
ret *= pow(10, scale);
}
}
return ret;
}
return val;
return NOVALUE;
}
time_t IEC6205675::getTimestamp(uint8_t* obis, int matchlength, const char* ptr) {

View File

@@ -3,6 +3,9 @@
#include "AmsData.h"
#include "ams/hdlc.h"
#include "AmsConfiguration.h"
#define NOVALUE 0xFFFFFFFF
struct AmsOctetTimestamp {
uint8_t type;
@@ -11,7 +14,7 @@ struct AmsOctetTimestamp {
class IEC6205675 : public AmsData {
public:
IEC6205675(const char* payload, uint8_t useMeterType, CosemDateTime packageTimestamp);
IEC6205675(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, CosemDateTime packageTimestamp, HDLCConfig* hc);
private:
CosemData* getCosemDataAt(uint8_t index, const char* ptr);
@@ -22,33 +25,19 @@ private:
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_VERSION[4] = { 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 };

57
src/MbusAssembler.cpp Normal file
View File

@@ -0,0 +1,57 @@
#include "Arduino.h"
#include "MbusAssembler.h"
#include "ams/hdlc.h"
MbusAssembler::MbusAssembler() {
buf = (uint8_t *)malloc((size_t)1024); // TODO find out from first package ?
}
uint8_t MbusAssembler::append(const uint8_t* d, int length) {
MbusHeader* h = (MbusHeader*) d;
uint8_t* ptr = (uint8_t*) &h[1];
uint8_t len = h->len1;
uint8_t control = *ptr;
ptr++; len--;
uint8_t address = *ptr;
ptr++; len--;
uint8_t ci = *ptr;
ptr++; len--;
uint8_t stsap = *ptr;
ptr++; len--;
uint8_t dtsap = *ptr;
ptr++; len--;
uint8_t sequenceNumber = ci & 0x0F;
if(sequenceNumber == 0) {
memcpy(buf, d, length - 2); // Do not include FCS and MBUS_STOP
buf[6] = 0x10; // Mark that this is a single, complete frame
pos = length - 2;
lastSequenceNumber = 0;
return 0;
} else if(pos + len > 1024 || sequenceNumber != (lastSequenceNumber + 1)) { // TODO return error
pos = 0;
lastSequenceNumber = -1;
return -1;
} else {
if(len > length) return -1;
memcpy(buf + pos, ptr, len);
pos += len;
lastSequenceNumber = sequenceNumber;
return 0;
}
return -2;
}
uint16_t MbusAssembler::write(const uint8_t* d) {
buf[1] = buf[2] = 0x00;
buf[pos++] = mbusChecksum(buf+4, pos-4);
buf[pos++] = MBUS_END;
memcpy((uint8_t *) d, buf, pos);
return pos;
}

18
src/MbusAssembler.h Normal file
View File

@@ -0,0 +1,18 @@
#ifndef _MBUS_ASSEMBLER_H
#define _MBUS_ASSEMBLER_H
#include <stdint.h>
class MbusAssembler {
public:
MbusAssembler();
uint8_t append(const uint8_t* d, int length);
uint16_t write(const uint8_t* d);
private:
uint16_t pos = 0;
uint8_t *buf;
uint8_t lastSequenceNumber = -1;
};
#endif

View File

@@ -16,217 +16,282 @@ void mbus_hexdump(const uint8_t* buf, int len) {
}
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)
uint8_t flag = *d;
uint8_t* ptr;
if(flag == HDLC_FLAG) {
if(length < 3)
return HDLC_FRAME_INCOMPLETE;
HDLCFooter* f = (HDLCFooter*) (d + len - sizeof *f);
footersize = sizeof *f;
HDLCHeader* h = (HDLCHeader*) d;
ptr = (uint8_t*) &h[1];
// First and last byte should be MBUS_HAN_TAG
if(h->flag != HDLC_FLAG || f->flag != HDLC_FLAG)
return HDLC_BOUNDRY_FLAG_MISSING;
// Frame format type 3
if((h->format & 0xF0) == 0xA0) {
// Length field (11 lsb of format)
len = (ntohs(h->format) & 0x7FF) + 2;
if(len > length)
return HDLC_FRAME_INCOMPLETE;
// Verify FCS
if(ntohs(f->fcs) != crc16_x25(d + 1, len - sizeof *f - 1))
return HDLC_FCS_ERROR;
HDLCFooter* f = (HDLCFooter*) (d + len - sizeof *f);
footersize = sizeof *f;
// Skip destination address, LSB marks last byte
while(((*ptr) & 0x01) == 0x00) {
ptr++;
// 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++;
}
headersize++;
ptr++;
// Skip source address, LSB marks last byte
while(((*ptr) & 0x01) == 0x00) {
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;
// Extract LLC
HDLCLLC* llc = (HDLCLLC*) ptr;
ptr += sizeof *llc;
headersize += sizeof *llc;
} else {
return HDLC_UNKNOWN_DATA;
}
headersize++;
ptr++;
} else if(flag == MBUS_START) {
// https://m-bus.com/documentation-wired/06-application-layer
if(length < 4)
return HDLC_FRAME_INCOMPLETE;
HDLC3CtrlHcs* t3 = (HDLC3CtrlHcs*) (ptr);
headersize += 3;
MbusHeader* mh = (MbusHeader*) d;
if(mh->flag1 != MBUS_START || mh->flag2 != MBUS_START)
return MBUS_BOUNDRY_FLAG_MISSING;
// Verify HCS
if(ntohs(t3->hcs) != crc16_x25(d + 1, ptr-d))
return HDLC_HCS_ERROR;
// First two bytes is 1-byte length value repeated. Only used for last segment
if(mh->len1 != mh->len2)
return MBUS_FRAME_LENGTH_NOT_EQUAL;
len = mh->len1;
ptr = (uint8_t*) &mh[1];
headersize = 4;
footersize = 2;
ptr += sizeof *t3;
} else if(h->flag == MBUS_START) {
// TODO: Check that the two next bytes are identical
if(len == 0x00)
len = length - headersize - footersize;
// Payload can max be 255 bytes, so I think the following case is only valid for austrian meters
if(len < headersize)
len += 256;
// Ignore: Control field + Address + Flag
if((headersize + footersize + len) > length)
return HDLC_FRAME_INCOMPLETE;
MbusFooter* mf = (MbusFooter*) (d + len + headersize);
if(mf->flag != MBUS_END)
return MBUS_BOUNDRY_FLAG_MISSING;
if(mbusChecksum(d + headersize, len) != mf->fcs)
return MBUS_CHECKSUM_ERROR;
ptr += 2;
// Control information field
uint8_t ci = *ptr;
// Bits 7 6 5 4 3 2 1 0
// 0 0 0 Finished Sequence number
uint8_t sequenceNumber = (ci & 0x0F);
if((ci & 0x10) == 0x00) { // Not finished yet
return MBUS_FRAME_INTERMEDIATE_SEGMENT;
} else if(sequenceNumber > 0) { // This is the last frame of multiple, assembly needed
return MBUS_FRAME_LAST_SEGMENT;
}
// Skip CI, STSAP and DTSAP
ptr += 3;
headersize += 3;
footersize++;
headersize += 5; // And also control and address that we didn't skip earlier, needed these for checksum.
} else {
return HDLC_UNKNOWN_DATA;
}
// Extract LLC
HDLCLLC* llc = (HDLCLLC*) ptr;
ptr += sizeof *llc;
headersize += 3;
Serial.flush();
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(((*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;
int ret = mbus_decrypt(ptr, length - headersize - footersize, config);
if(ret < 0) return ret;
ptr += ret;
}
uint8_t systemTitleLength = *ptr;
ptr++;
memcpy(config->system_title, ptr, systemTitleLength);
memcpy(config->initialization_vector, config->system_title, systemTitleLength);
HDLCADPU* adpu = (HDLCADPU*) (ptr);
ptr += sizeof *adpu;
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;
// ADPU timestamp
CosemData* dateTime = (CosemData*) ptr;
if(dateTime->base.type == CosemTypeOctetString) {
if(dateTime->base.length == 0x0C) {
memcpy(timestamp, ptr+1, dateTime->base.length+1);
}
//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 += 2 + dateTime->base.length;
} else if(dateTime->base.type == CosemTypeNull) {
timestamp = 0;
ptr++;
headersize++;
} else if(dateTime->base.type == CosemTypeDateTime) {
memcpy(timestamp, ptr, dateTime->base.length);
} else if(dateTime->base.type == 0x0C) { // Kamstrup bug...
memcpy(timestamp, ptr, 13);
ptr += 13;
} else {
return HDLC_TIMESTAMP_UNKNOWN;
}
// Frame counter
memcpy(config->initialization_vector + 8, ptr, 4);
ptr += 4;
headersize += 4;
return ptr-d;
}
// 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);
int mbus_decrypt(const uint8_t* d, int length, HDLCConfig* config) {
if(length < 12) return HDLC_FRAME_INCOMPLETE;
uint8_t* ptr = (uint8_t*) d;
if(*ptr != 0xDB) return HDLC_ENCRYPTION_INVALID;
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);
int len;
int 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;
// 2-byte payload length
len = (ntohs(h->format) & 0xFFFF);
ptr += 3;
headersize += 3;
}
if(len + headersize > length)
return HDLC_FRAME_INCOMPLETE;
//Serial.printf("\nL: %d : %d, %d\n", length, len, headersize);
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;
int footersize = 0;
// 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 - 5, 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);
#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;
}
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;
}
if (0 < authkeylen) {
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) {
mbedtls_gcm_free(&m_ctx);
return HDLC_ENCRYPTION_AUTH_FAILED;
} else if(success == MBEDTLS_ERR_GCM_BAD_INPUT) {
mbedtls_gcm_free(&m_ctx);
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;
success = mbedtls_gcm_starts(&m_ctx, MBEDTLS_GCM_DECRYPT, config->initialization_vector, sizeof(config->initialization_vector),NULL, 0);
if (0 != success) {
mbedtls_gcm_free(&m_ctx);
return HDLC_ENCRYPTION_DECRYPT_FAILED;
}
success = mbedtls_gcm_update(&m_ctx, sizeof(cipher_text), cipher_text, (unsigned char*)(ptr));
if (0 != success) {
mbedtls_gcm_free(&m_ctx);
return HDLC_ENCRYPTION_DECRYPT_FAILED;
}
}
mbedtls_gcm_free(&m_ctx);
#endif
return ptr-d;
}
// Unknown payload
return HDLC_UNKNOWN_DATA;
return ptr-d;
}
uint8_t mbusChecksum(const uint8_t* p, int len) {
uint8_t ret = 0;
while(len--)
ret += *p++;
return ret;
}
uint64_t ntohll(uint64_t x) {
return (((uint64_t)ntohl((uint32_t)x)) << 32) + ntohl(x >> 32);
}

View File

@@ -3,6 +3,7 @@
#include "Arduino.h"
#include <stdint.h>
#include "lwip/def.h"
#define HDLC_FLAG 0x7E
#define HDLC_BOUNDRY_FLAG_MISSING -1
@@ -14,9 +15,16 @@
#define HDLC_ENCRYPTION_AUTH_FAILED -91
#define HDLC_ENCRYPTION_KEY_FAILED -92
#define HDLC_ENCRYPTION_DECRYPT_FAILED -93
#define HDLC_ENCRYPTION_INVALID -98
#define HDLC_TIMESTAMP_UNKNOWN -99
#define MBUS_START 0x68
#define MBUS_END 0x16
#define MBUS_BOUNDRY_FLAG_MISSING -1
#define MBUS_FRAME_LENGTH_NOT_EQUAL -40
#define MBUS_FRAME_INTERMEDIATE_SEGMENT -41
#define MBUS_FRAME_LAST_SEGMENT -42
#define MBUS_CHECKSUM_ERROR -2
struct HDLCConfig {
uint8_t encryption_key[32];
@@ -53,6 +61,12 @@ typedef struct HDLCADPU {
uint32_t id;
} __attribute__((packed)) HDLCADPU;
typedef struct MbusHeader {
uint8_t flag1;
uint8_t len1;
uint8_t len2;
uint8_t flag2;
} __attribute__((packed)) MbusHeader;
typedef struct MbusFooter {
uint8_t fcs;
@@ -67,9 +81,12 @@ enum CosemType {
CosemTypeStructure = 0x02,
CosemTypeOctetString = 0x09,
CosemTypeString = 0x0A,
CosemTypeDLongSigned = 0x05,
CosemTypeDLongUnsigned = 0x06,
CosemTypeLongSigned = 0x10,
CosemTypeLongUnsigned = 0x12,
CosemTypeLong64Signed = 0x14,
CosemTypeLong64Unsigned = 0x15,
CosemTypeDateTime = 0x19
};
@@ -84,19 +101,34 @@ struct CosemString {
uint8_t data[];
} __attribute__((packed));
struct CosemLongSigned {
uint8_t type;
int16_t data;
} __attribute__((packed));
struct CosemLongUnsigned {
uint8_t type;
uint16_t data;
} __attribute__((packed));
struct CosemDLongSigned {
uint8_t type;
int32_t data;
} __attribute__((packed));
struct CosemDLongUnsigned {
uint8_t type;
uint32_t data;
} __attribute__((packed));
struct CosemLongSigned {
struct CosemLong64Signed {
uint8_t type;
int16_t data;
int64_t data;
} __attribute__((packed));
struct CosemLong64Unsigned {
uint8_t type;
uint64_t data;
} __attribute__((packed));
struct CosemDateTime {
@@ -117,13 +149,21 @@ typedef union {
struct CosemBasic base;
struct CosemString str;
struct CosemString oct;
struct CosemLongUnsigned lu;
struct CosemDLongUnsigned dlu;
struct CosemLongSigned ls;
struct CosemLongUnsigned lu;
struct CosemDLongSigned dls;
struct CosemDLongUnsigned dlu;
struct CosemLong64Signed l64s;
struct CosemLong64Unsigned l64u;
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);
int HDLC_validate(const uint8_t* d, int length, HDLCConfig* config, CosemDateTime* timestamp);
int mbus_decrypt(const uint8_t* d, int length, HDLCConfig* config);
uint8_t mbusChecksum(const uint8_t* p, int len);
uint64_t ntohll(uint64_t x);
#endif

View File

@@ -1,4 +1,5 @@
#include "DnbCurrParser.h"
#include "Arduino.h"
#include "HardwareSerial.h"
float DnbCurrParser::getValue() {
@@ -29,13 +30,29 @@ size_t DnbCurrParser::write(const uint8_t *buffer, size_t size) {
}
size_t DnbCurrParser::write(uint8_t byte) {
if(pos >= 64) pos = 0;
if(pos == 0) {
if(byte == '<') {
buf[pos++] = byte;
}
} else if(byte == '>') {
buf[pos++] = byte;
if(strncmp(buf, "<Obs", 4) == 0) {
if(strncmp(buf, "<Series", 7) == 0) {
for(int i = 0; i < pos; i++) {
if(strncmp(buf+i, "UNIT_MULT=\"", 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;
}
}
scale = String(buf+pos).toInt();
} else if(strncmp(buf, "<Obs", 4) == 0) {
for(int i = 0; i < pos; i++) {
if(strncmp(buf+i, "OBS_VALUE=\"", 11) == 0) {
pos = i + 11;
@@ -49,7 +66,7 @@ size_t DnbCurrParser::write(uint8_t byte) {
break;
}
}
value = String(buf+pos).toFloat();
value = String(buf+pos).toFloat() / pow(10, scale);
}
pos = 0;
} else {

View File

@@ -15,6 +15,7 @@ public:
size_t write(uint8_t);
private:
uint8_t scale = 0;
float value = 1.0;
char buf[64];

View File

@@ -2,7 +2,7 @@
#include "HardwareSerial.h"
EntsoeA44Parser::EntsoeA44Parser() {
for(int i = 0; i < 24; i++) points[i] = 0.0;
for(int i = 0; i < 24; i++) points[i] = ENTSOE_NO_VALUE;
}
char* EntsoeA44Parser::getCurrency() {
@@ -14,6 +14,7 @@ char* EntsoeA44Parser::getMeasurementUnit() {
}
float EntsoeA44Parser::getPoint(uint8_t position) {
if(position >= 24) return ENTSOE_NO_VALUE;
return points[position];
}
@@ -41,6 +42,7 @@ size_t EntsoeA44Parser::write(const uint8_t *buffer, size_t size) {
}
size_t EntsoeA44Parser::write(uint8_t byte) {
if(pos >= 64) pos = 0;
if(docPos == DOCPOS_CURRENCY) {
buf[pos++] = byte;
if(pos == 3) {

View File

@@ -9,6 +9,8 @@
#define DOCPOS_POSITION 3
#define DOCPOS_AMOUNT 4
#define ENTSOE_NO_VALUE -127
class EntsoeA44Parser: public Stream {
public:
EntsoeA44Parser();

View File

@@ -1,24 +1,25 @@
#include "EntsoeApi.h"
#include <EEPROM.h>
#include "Uptime.h"
#include "Time.h"
#include "TimeLib.h"
#include "DnbCurrParser.h"
#include "version.h"
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
#include <HTTPClient.h>
#else
#warning "Unsupported board type"
#if defined(ESP32)
#include <esp_task_wdt.h>
#endif
EntsoeApi::EntsoeApi(RemoteDebug* Debug) {
this->buf = (char*) malloc(BufferSize);
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);
tomorrowFetchMillis = 36000000 + (random(1800) * 1000); // Random between 13:30 and 14:00
}
void EntsoeApi::setup(EntsoeConfig& config) {
@@ -26,6 +27,7 @@ void EntsoeApi::setup(EntsoeConfig& config) {
this->config = new EntsoeConfig();
}
memcpy(this->config, &config, sizeof(config));
lastCurrencyFetch = 0;
}
char* EntsoeApi::getToken() {
@@ -36,12 +38,12 @@ char* EntsoeApi::getCurrency() {
return this->config->currency;
}
float EntsoeApi::getValueForHour(uint8_t hour) {
float EntsoeApi::getValueForHour(int8_t hour) {
time_t cur = time(nullptr);
return getValueForHour(cur, hour);
}
float EntsoeApi::getValueForHour(time_t cur, uint8_t hour) {
float EntsoeApi::getValueForHour(time_t cur, int8_t hour) {
tmElements_t tm;
if(tz != NULL)
cur = tz->toLocal(cur);
@@ -56,22 +58,26 @@ float EntsoeApi::getValueForHour(time_t cur, uint8_t hour) {
if(tomorrow == NULL)
return ENTSOE_NO_VALUE;
value = tomorrow->getPoint(pos-24);
if(strcmp(tomorrow->getMeasurementUnit(), "MWH") == 0) {
if(value != ENTSOE_NO_VALUE && strcmp(tomorrow->getMeasurementUnit(), "MWH") == 0) {
multiplier *= 0.001;
} else {
return ENTSOE_NO_VALUE;
}
multiplier *= getCurrencyMultiplier(tomorrow->getCurrency(), config->currency);
} else {
float mult = getCurrencyMultiplier(tomorrow->getCurrency(), config->currency);
if(mult == 0) return ENTSOE_NO_VALUE;
multiplier *= mult;
} else if(pos >= 0) {
if(today == NULL)
return ENTSOE_NO_VALUE;
value = today->getPoint(pos);
if(strcmp(today->getMeasurementUnit(), "MWH") == 0) {
if(value != ENTSOE_NO_VALUE && strcmp(today->getMeasurementUnit(), "MWH") == 0) {
multiplier *= 0.001;
} else {
return ENTSOE_NO_VALUE;
}
multiplier *= getCurrencyMultiplier(today->getCurrency(), config->currency);
float mult = getCurrencyMultiplier(today->getCurrency(), config->currency);
if(mult == 0) return ENTSOE_NO_VALUE;
multiplier *= mult;
}
return value * multiplier;
}
@@ -79,87 +85,108 @@ float EntsoeApi::getValueForHour(time_t cur, uint8_t hour) {
bool EntsoeApi::loop() {
if(strlen(getToken()) == 0)
return false;
bool ret = false;
uint64_t now = millis64();
if(now < 10000) return false; // Grace period
time_t t = time(nullptr);
if(t < BUILD_EPOCH) return false;
bool ret = false;
tmElements_t tm;
breakTime(tz->toLocal(t), tm);
if(currentHour != tm.Hour) {
currentHour = tm.Hour;
ret = today != NULL; // Only trigger MQTT publish if we have todays prices.
}
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));
uint32_t curDayMillis = (((((tm.Hour * 60) + tm.Minute) * 60) + tm.Second) * 1000);
midnightMillis = now + (SECS_PER_DAY * 1000) - curDayMillis;
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Setting midnight millis %lu\n", midnightMillis);
currentDay = tm.Day;
return false;
} else if(now > midnightMillis && currentDay != tm.Day) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Rotating price objects at %lu\n", t);
if(today != NULL) delete today;
if(tomorrow != NULL) {
today = tomorrow;
tomorrow = NULL;
}
} else if(now > midnightMillis) {
printI("Rotating price objects");
delete today;
today = tomorrow;
tomorrow = NULL;
currentDay = tm.Day;
midnightMillis = 0; // Force new midnight millis calculation
return true;
} else {
breakTime(t, tm); // Break UTC to find UTC midnight
if(today == NULL && (lastTodayFetch == 0 || now - lastTodayFetch > 60000)) {
lastTodayFetch = now;
time_t e1 = time(nullptr) - (SECS_PER_DAY * 1);
time_t e1 = t - (tm.Hour * 3600) - (tm.Minute * 60) - tm.Second; // UTC midnight
time_t e2 = e1 + SECS_PER_DAY;
tmElements_t d1, d2;
breakTime(e1, d1);
breakTime(e2, d2);
breakTime(tz->toUTC(e1), d1); // To get day and hour for CET/CEST at UTC midnight
breakTime(tz->toUTC(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",
snprintf(buf, BufferSize, "%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,
d1.Year+1970, d1.Month, d1.Day, d1.Hour, 00,
d2.Year+1970, d2.Month, d2.Day, d2.Hour, 00,
config->area, config->area);
printI("Fetching prices for today");
printD(url);
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Fetching prices for today\n");
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf);
EntsoeA44Parser* a44 = new EntsoeA44Parser();
if(retrieve(url, a44)) {
if(retrieve(buf, a44) && a44->getPoint(0) != ENTSOE_NO_VALUE) {
today = a44;
ret = true;
} else {
return true;
} else if(a44 != NULL) {
delete a44;
today = NULL;
return false;
}
}
// Prices for next day are published at 13:00 CE(S)T, but to avoid heavy server traffic at that time, we will
// fetch 1 hr after that (with some random delay) and retry every 15 minutes
if(tomorrow == NULL
&& midnightMillis - now < 39600000
&& (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 60000)
&& midnightMillis - now < tomorrowFetchMillis
&& (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 900000)
) {
lastTomorrowFetch = now;
time_t e1 = time(nullptr);
time_t e1 = t - (tm.Hour * 3600) - (tm.Minute * 60) - tm.Second + (SECS_PER_DAY);
time_t e2 = e1 + SECS_PER_DAY;
tmElements_t d1, d2;
breakTime(e1, d1);
breakTime(e2, d2);
breakTime(tz->toUTC(e1), d1);
breakTime(tz->toUTC(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",
snprintf(buf, BufferSize, "%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,
d1.Year+1970, d1.Month, d1.Day, d1.Hour, 00,
d2.Year+1970, d2.Month, d2.Day, d2.Hour, 00,
config->area, config->area);
printI("Fetching prices for tomorrow");
printD(url);
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Fetching prices for tomorrow\n");
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf);
EntsoeA44Parser* a44 = new EntsoeA44Parser();
if(retrieve(url, a44)) {
if(retrieve(buf, a44) && a44->getPoint(0) != ENTSOE_NO_VALUE) {
tomorrow = a44;
ret = true;
} else {
return true;
} else if(a44 != NULL) {
delete a44;
tomorrow = NULL;
return false;
}
}
}
@@ -167,71 +194,47 @@ bool EntsoeApi::loop() {
}
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);
https.setReuse(false);
https.setTimeout(50000);
https.setUserAgent("ams2mqtt/" + String(VERSION));
#if defined(ESP32)
if(https.begin(url)) {
printD("Connection established");
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);
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
https.end();
int status = https.GET();
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
if(status == HTTP_CODE_OK) {
printD("Receiving data");
https.writeToStream(doc);
https.end();
return true;
} else {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("(EntsoeApi) Communication error, returned status: %d\n", status);
printE(https.errorToString(status));
printD(https.getString());
https.end();
return false;
}
} else {
return false;
}
} else {
#if defined(ESP8266)
char buf[64];
client.getLastSSLError(buf,64);
printE(buf);
#endif
return false;
}
client.stop();
#endif
return false;
}
float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to) {
@@ -239,18 +242,39 @@ float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to) {
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();
}
if(now > lastCurrencyFetch && (lastCurrencyFetch == 0 || (now - lastCurrencyFetch) > 60000)) {
lastCurrencyFetch = now;
DnbCurrParser p;
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
snprintf(buf, BufferSize, "https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1", from);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Retrieving %s to NOK conversion\n", from);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf);
if(retrieve(buf, &p)) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue());
currencyMultiplier = p.getValue();
if(strncmp(to, "NOK", 3) != 0) {
snprintf(buf, BufferSize, "https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1", to);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Retrieving %s to NOK conversion\n", to);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf);
if(retrieve(buf, &p)) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue());
currencyMultiplier /= p.getValue();
} else {
return 0;
}
}
} else {
return 0;
}
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) Resulting currency multiplier: %.4f\n", currencyMultiplier);
lastCurrencyFetch = midnightMillis;
}
return currencyMultiplier;
}
@@ -262,20 +286,6 @@ void EntsoeApi::printD(String fmt, ...) {
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);

View File

@@ -1,14 +1,20 @@
#ifndef _ENTSOEAPI_H
#define _ENTSOEAPI_H
#include "time.h"
#include "TimeLib.h"
#include "Timezone.h"
#include "RemoteDebug.h"
#include "EntsoeA44Parser.h"
#include "AmsConfiguration.h"
#define ENTSOE_NO_VALUE -127
#define ENTSOE_DEFAULT_MULTIPLIER 1.00
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
#include <HTTPClient.h>
#else
#warning "Unsupported board type"
#endif
#define SSL_BUF_SIZE 512
class EntsoeApi {
@@ -19,13 +25,15 @@ public:
char* getToken();
char* getCurrency();
float getValueForHour(uint8_t);
float getValueForHour(time_t, uint8_t);
float getValueForHour(int8_t);
float getValueForHour(time_t, int8_t);
private:
RemoteDebug* debugger;
EntsoeConfig* config = NULL;
uint8_t currentDay = 0, currentHour = 0;
uint32_t tomorrowFetchMillis = 36000000; // Number of ms before midnight. Default fetch 10hrs before midnight (14:00 CE(S)T)
uint64_t midnightMillis = 0;
uint64_t lastTodayFetch = 0;
uint64_t lastTomorrowFetch = 0;
@@ -35,14 +43,15 @@ private:
Timezone* tz = NULL;
float currencyMultiplier = ENTSOE_DEFAULT_MULTIPLIER;
static const uint16_t BufferSize = 256;
char* buf;
float currencyMultiplier = 0;
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

View File

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

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