Compare commits

...

137 Commits

Author SHA1 Message Date
Gunnar Skjold
4e97554514 Some changes during testing 2022-12-14 19:37:32 +01:00
Gunnar Skjold
28a9d6746b Fixed discover when setting different hostname in setup 2022-12-14 18:15:46 +01:00
Gunnar Skjold
7ea4fe881c HAN serial autodetect 2022-12-13 21:05:42 +01:00
Gunnar Skjold
c4eaf8184b Check if authenticated to show upgrade links and config up/download 2022-12-10 09:27:18 +01:00
Gunnar Skjold
312972f77d Some improvements to v2.2 2022-12-09 20:26:48 +01:00
Gunnar Skjold
27b9058af5 Some improvements to v2.2 2022-12-09 20:26:31 +01:00
Gunnar Skjold
bc4d61098c Some error handling 2022-12-09 19:10:24 +01:00
Gunnar Skjold
b2de6472cf Some experiments with captive portal 2022-12-09 18:24:43 +01:00
Gunnar Skjold
6c3ddc57b5 Removed "auto multiplier" for Kamstrup 2022-12-08 13:44:21 +01:00
Gunnar Skjold
2ff7044c85 Fixed incorrect value for peaks in MQTT 2022-12-07 20:13:30 +01:00
Gunnar Skjold
0145be851e Merge branch 'master' into dev-v2.2 2022-12-07 20:11:14 +01:00
Gunnar Skjold
6b0d540f39 Added 300 baud 2022-12-07 11:51:45 +01:00
Gunnar Skjold
2218ac4e8a Fixed graph clearing problem 2022-12-07 11:32:55 +01:00
Gunnar Skjold
33bd3da310 Various fix for realtime values 2022-12-06 15:45:43 +01:00
Gunnar Skjold
a239e1a63d Fix on special case for Kaifa MA304T3 2022-12-06 08:28:30 +01:00
Gunnar Skjold
8a809ec128 Bugfix from testing 2022-12-04 14:32:47 +01:00
Gunnar Skjold
b48a0f13fe More v2.2 2022-12-04 14:12:35 +01:00
Gunnar Skjold
b07ed075f4 Even more v2.2 2022-12-02 20:38:56 +01:00
Gunnar Skjold
0927cab8e2 Some changes for fetching prices from amshub 2022-12-02 19:24:53 +01:00
Gunnar Skjold
eed35b7bbc Merge branch 'hub-prices' into dev-v2.2 2022-12-02 19:08:29 +01:00
Gunnar Skjold
e34da5fd83 Merge branch 'master' into hub-prices 2022-12-02 19:04:14 +01:00
Gunnar Skjold
148fb14c93 More v2.2 2022-12-02 19:03:16 +01:00
Gunnar Skjold
000cfd8697 Merge branch 'master' into dev-v2.2 2022-12-01 18:12:32 +01:00
Gunnar Skjold
1ef5703971 Fixed changing mdns settings 2022-12-01 18:06:19 +01:00
Gunnar Skjold
cda3b80b35 Some changes 2022-11-29 21:24:58 +01:00
Gunnar Skjold
c7b8090634 Some minor changes 2022-11-29 21:02:23 +01:00
Gunnar Skjold
63a8d79b95 Making it work on 8266 2022-11-29 20:50:36 +01:00
Gunnar Skjold
02ae3fc7f5 More v2.2 2022-11-29 20:20:56 +01:00
Gunnar Skjold
2dcc874592 More v2.2 2022-11-25 19:18:32 +01:00
Gunnar Skjold
d4d9d2224f More v2.2 2022-11-23 20:52:37 +01:00
Gunnar Skjold
902e43979b Even more v2.2 2022-11-21 18:48:04 +01:00
Gunnar Skjold
8e54f23367 More v2.2 2022-11-20 20:47:29 +01:00
Gunnar Skjold
ab7128c53a Further work on v2.2 2022-11-19 15:44:23 +01:00
Gunnar Skjold
6563700df4 Merge branch 'master' into dev-v2.2 2022-11-15 18:07:02 +01:00
Gunnar Skjold
538de5ea99 Increased range of multipliers 2022-11-08 18:41:31 +01:00
Gunnar Skjold
042e2bcc85 Fixed power saving 2022-11-07 14:57:53 +01:00
Gunnar Skjold
b6f630b134 Some more Svelte implementation 2022-11-01 20:59:38 +01:00
Gunnar Skjold
65a09dcecf Merge branch 'master' into dev-v2.2 2022-11-01 19:07:00 +01:00
Gunnar Skjold
775e5a0881 Support different size numbers in L&G payload 2022-11-01 18:41:43 +01:00
Gunnar Skjold
6c0d5fcc09 Some adjustments to wifi power saving option 2022-10-31 10:09:07 +01:00
Gunnar Skjold
69d8fa9254 Made wifi sleep mode configurable 2022-10-27 19:17:50 +02:00
root
71d261bf34 Merge branch 'hub-prices' of github.com:gskjold/AmsReader into hub-prices 2022-10-23 19:11:51 +02:00
root
9f4f5b4620 Merge branch 'master' into hub-prices 2022-10-23 19:09:15 +02:00
Gunnar Skjold
c38c305bab Fixed threshold update via config file 2022-10-21 19:21:55 +02:00
Gunnar Skjold
bfee2a1d64 Fixed month average calculation 2022-10-21 18:43:30 +02:00
Gunnar Skjold
ec7edae9a1 Updated issue templates 2022-10-21 17:43:33 +02:00
Gunnar Skjold
d8e265b7ac Fixed JSON errors 2022-10-20 20:12:33 +02:00
Gunnar Skjold
1987cddab7 Some more cleanup 2022-10-20 20:11:13 +02:00
Gunnar Skjold
e18be5f97c Better memory optimization 2022-10-19 20:58:57 +02:00
Gunnar Skjold
537597d6d1 Expanding size of daily and monthly data points 2022-10-19 20:30:29 +02:00
Gunnar Skjold
a89013cec3 Fixed config file upload 2022-10-19 20:24:21 +02:00
Gunnar Skjold
9c7a0cb7ff Fixed graph error 2022-10-19 19:59:41 +02:00
Gunnar Skjold
ade12199b9 Reclaiming some heap space 2022-10-19 19:54:26 +02:00
Gunnar Skjold
92692c6eaf More implementation of Svelte GUI 2022-10-18 20:52:42 +02:00
Gunnar Skjold
e4114c3e74 Merge branch 'master' into dev-v2.2 2022-10-18 18:30:37 +02:00
Gunnar Skjold
39b68aca51 Removed invalid code 2022-10-18 18:30:26 +02:00
root
feffbb53a3 Merge branch 'master' of github.com:gskjold/AmsToMqttBridge into hub-prices 2022-10-17 15:42:23 +02:00
Gunnar Skjold
8ac1e034b1 Reverted WiFi_connect method 2022-10-17 13:09:28 +02:00
Gunnar Skjold
f446dff865 Fixed LED blink delay 2022-10-17 13:09:04 +02:00
root
e1b2554af2 Merge branch 'master' of github.com:gskjold/AmsToMqttBridge into hub-prices 2022-10-16 18:46:47 +02:00
Gunnar Skjold
627a50ab50 Fixed priceapi dropdowns 2022-10-16 18:46:31 +02:00
Gunnar Skjold
a20d007b45 Merge branch 'hub-prices' of github.com:gskjold/AmsReader into hub-prices 2022-10-16 18:32:25 +02:00
Gunnar Skjold
64840e13f0 Merge branch 'master' into hub-prices 2022-10-16 18:31:26 +02:00
Gunnar Skjold
34ebe9601a Changed blink behaviour 2022-10-16 18:31:11 +02:00
root
72f1d59338 Merge branch 'hub-prices' of github.com:gskjold/AmsReader into hub-prices 2022-10-16 17:36:48 +02:00
Gunnar Skjold
07ed425320 Merge branch 'master' into hub-prices 2022-10-16 17:34:19 +02:00
Gunnar Skjold
2e75e1c4dc Moved HA files 2022-10-13 20:58:08 +02:00
Gunnar Skjold
e6df68481f Merge branch 'master' into dev-v2.2 2022-10-13 20:47:48 +02:00
Gunnar Skjold
fa299198fc Fixed all build warnings 2022-10-13 20:33:59 +02:00
Gunnar Skjold
6d81b0a856 Fixed all build warnings 2022-10-13 20:24:56 +02:00
Gunnar Skjold
dd095da97b Reduced footprint of GPIO config page 2022-10-13 19:27:25 +02:00
Gunnar Skjold
1b6ce203b7 Better debug output whem receiving unknown data 2022-10-13 17:40:44 +02:00
Gunnar Skjold
2850be4e48 Merge pull request #322 from tbarnekov/extend_power_info
Populate Active Import and Export per phase and export to HA
2022-10-12 20:34:09 +02:00
Gunnar Skjold
7eca31de84 Fixed build errors 2022-10-12 20:01:55 +02:00
Gunnar Skjold
a64f960cc7 Added Pow-U+ profile 2022-10-12 19:30:00 +02:00
Gunnar Skjold
ce3a47a7e6 Fixed error blinks and improved WiFi reconnect 2022-10-12 19:25:48 +02:00
Thomas Barnekov
8395e1dc77 Merge remote-tracking branch 'origin/master' into extend_power_info 2022-10-10 17:06:09 +02:00
Thomas Barnekov
39a4761415 Add DOCTYPE to html to fix quirks mode 2022-10-10 16:59:43 +02:00
root
dc25c147b9 Merge remote-tracking branch 'public/master' into hub-prices 2022-10-07 17:36:49 +00:00
Gunnar Skjold
867ab9d6c2 Build variable for upgrade URL 2022-10-07 19:35:30 +02:00
Gunnar Skjold
6a76144566 Merge branch 'master' into hub-prices 2022-10-07 17:49:47 +02:00
Thomas Barnekov
5e03e3d3c2 Move per-phase power consumption and power factor values to new ha4.json 2022-10-07 13:40:29 +02:00
Thomas Barnekov
adb5050621 Merge branch 'gskjold:master' into extend_power_info 2022-10-06 17:31:43 +02:00
Gunnar Skjold
026cd25c8c Setting modem sleep to max 2022-10-06 17:30:37 +02:00
Gunnar Skjold
06ec97b42a Make sure we dont use more than 5 peaks if defined higher 2022-10-06 17:20:13 +02:00
Gunnar Skjold
b420a0e6f4 Limit peak count if over 5 2022-10-06 17:12:12 +02:00
Thomas Barnekov
7cd52d5689 Add individual power reading for all phases (include in HA) 2022-10-06 00:35:30 +02:00
Thomas Barnekov
ad78ff3082 Fix implicit cast and non-const char pointer warnings 2022-10-05 22:42:43 +02:00
Gunnar Skjold
fe7be81f1e Rudimentary detection for L&G data. Probably change this in the future 2022-10-01 12:11:46 +02:00
Gunnar Skjold
7674fc2ad0 Merge branch 'lng-parser' 2022-10-01 11:56:08 +02:00
Gunnar Skjold
d50181c347 Upgrade with custom URL 2022-10-01 11:43:42 +02:00
Gunnar Skjold
8ca771fa5a Changed "BUS" to "M-BUS" 2022-10-01 09:25:56 +02:00
Gunnar Skjold
7d557b2679 Only show new version when actually new in firmware page. Also be more specific on USB power in upgrade warning 2022-10-01 09:24:07 +02:00
Gunnar Skjold
2f0c912388 Exclude decimals in BUILD_EPOCH flag 2022-10-01 09:22:09 +02:00
Gunnar Skjold
57e6d0fbe3 Modifications to make price sensors work in HA 2022-09-30 19:18:23 +02:00
Gunnar Skjold
e7c25fafda Prices and realtime HA sensors 2022-09-28 20:13:03 +02:00
Gunnar Skjold
2a4772fe25 Fixed memory leak when reconnecting to MQTT/SSL 2022-09-27 19:59:37 +02:00
Gunnar Skjold
f1f7408208 Added price zones 2022-09-25 11:46:04 +02:00
Gunnar Skjold
feb8e5007b Fixed MQTT buffer size 2022-09-25 10:50:34 +02:00
Gunnar Skjold
c4af1ee74f Fixed saving tariff thresholds 2022-09-25 10:50:09 +02:00
Gunnar Skjold
992e1b6121 Net value for active power (L&G) 2022-09-12 08:03:32 +02:00
Gunnar Skjold
95d3008a66 Updates to Svelte UI 2022-09-12 07:29:25 +02:00
Gunnar Skjold
04cd6ac387 Some changes to make Svelte UI work 2022-09-03 14:37:31 +02:00
Gunnar Skjold
584c7b1154 More restructuring 2022-09-03 14:03:15 +02:00
Gunnar Skjold
493c4b4337 Moved Svelte UI to lib folder 2022-09-03 13:41:11 +02:00
Gunnar Skjold
488c969858 Some serious restructuring to be able to swap between implementations 2022-09-03 13:12:29 +02:00
Gunnar Skjold
8b0d4185d3 Created new UI with Svelte 2022-09-03 12:19:56 +02:00
Gunnar Skjold
9cc7529934 Fixed L&G scaling 2022-08-31 08:19:09 +02:00
Gunnar Skjold
ef8715be6d Merge branch 'master' into lng-parser 2022-08-31 08:04:24 +02:00
Gunnar Skjold
e232b875fa Fixed Aidon timestamp 2022-08-23 21:07:06 +02:00
Gunnar Skjold
f18171fecc Fixed MQTT byte mode 2022-08-23 20:39:47 +02:00
Gunnar Skjold
a3c7a09211 Adding peaks to data.json and MQTT RAW 2022-08-23 20:21:19 +02:00
Gunnar Skjold
a6f3bc3f71 Added version to system json 2022-08-23 19:49:19 +02:00
Gunnar Skjold
44bcd386d1 aggregate obis 1.8.x and 2.8.x for L&G 2022-08-23 09:04:45 +02:00
Gunnar Skjold
01547f9a52 Adjustments to make L&G parser work 2022-08-19 13:26:29 +02:00
Gunnar Skjold
608470c5f7 Merge branch 'master' into hub-prices 2022-08-18 19:56:31 +02:00
Gunnar Skjold
940d38af5c Merge branch 'master' into lng-parser 2022-08-17 17:33:24 +02:00
Gunnar Skjold
4e451c51e1 HW serial fix for S2 2022-08-17 17:33:06 +02:00
Gunnar Skjold
43def1c311 Fixed Pow-P1 profile 2022-08-17 17:32:53 +02:00
Gunnar Skjold
2978116207 Fixed formatting error in HA payload 2022-08-16 11:55:07 +02:00
Gunnar Skjold
a055465ce0 Untestet L&G data parser 2022-08-16 08:22:43 +02:00
Gunnar Skjold
68b3415a6c Cleanup for price API 2022-08-13 16:32:55 +02:00
Gunnar Skjold
d8f3ae8b07 Merge branch 'master' into hub-prices 2022-08-13 16:00:25 +02:00
Gunnar Skjold
998b986604 Adjusted modem sleep to a safer value 2022-08-12 20:48:34 +02:00
Gunnar Skjold
7799431405 Remove unused files 2022-08-12 20:42:13 +02:00
Gunnar Skjold
1a92cd1978 Fetch prices from hub 2022-08-12 20:41:53 +02:00
Gunnar Skjold
508c14a671 Fixed modem sleep on S2 and fixed default wifi power 2022-08-12 09:02:04 +02:00
Gunnar Skjold
4a7ef87269 Fixed realtime day calculation 2022-08-10 17:43:51 +02:00
Gunnar Skjold
3a4fc707b0 Show real time production 2022-08-10 10:53:16 +02:00
Gunnar Skjold
5d2e320b07 Fixed export labels on graph 2022-08-10 10:28:31 +02:00
Gunnar Skjold
d12613b91a Trying to fix HA accumulated 2022-08-10 10:01:24 +02:00
Gunnar Skjold
e6a02f34ab USB power warning when upgrading 2022-08-10 09:31:50 +02:00
Gunnar Skjold
0b2ffbfd77 Revert to arduino 2.0.3 because of #298 2022-08-10 09:30:31 +02:00
Gunnar Skjold
cab6c54ed9 Correct Vcc GPIO for Pow-K+ 2022-08-01 12:48:31 +02:00
Gunnar Skjold
313024f273 Support OTA upgrade from ESP8266 2022-08-01 08:17:24 +02:00
Gunnar Skjold
91fc078c5e Higher precision on prices 2022-07-31 20:26:38 +02:00
Gunnar Skjold
5b9d44a3e9 Fixed loading of energy accounting 2022-07-31 20:19:01 +02:00
200 changed files with 13164 additions and 1478 deletions

View File

@@ -33,6 +33,7 @@ If applicable, add screenshots to help explain your problem.
**Relevant firmware information:**
- Version: [e.g. 2.1.0]
- MQTT: [yes/no]
- MQTT payload type: [e.g. JSON]
- HAN GPIO: [e.g. GPIO5]
- HAN baud and parity: [e.g. 2400 8E1]
- Temperature sensors [e.g. 3xDS18B20]

View File

@@ -20,6 +20,7 @@ A clear and concise description of what the problem is.
**Relevant firmware information:**
- Version: [e.g. 2.1.0]
- MQTT: [yes/no]
- MQTT payload type: [e.g. JSON]
- HAN GPIO: [e.g. GPIO5]
- HAN baud and parity: [e.g. 2400 8E1]
- Temperature sensors [e.g. 3xDS18B20]

5
.gitignore vendored
View File

@@ -7,7 +7,7 @@
.vscode
.pio
platformio-user.ini
/src/version.h
/lib/AmsConfiguration/include/version.h
/src/web/root
/src/AmsToMqttBridge.ino.cpp
/test
@@ -15,3 +15,6 @@ platformio-user.ini
/sdkconfig
/.tmp
/*.zip
node_modules
/gui/dist
/scripts/*dev

View File

@@ -43,3 +43,37 @@ FF // Last byte of OBIS in previous block
0600000000 // Accumulated export
8BA4
7E
7E A1 23 CE FF 03 13 21 55 E6 E7 00
0F 00 00 08 E2
0C 07 E5 07 13 01 0C 1A 0A FF 80 00 00
02 0B // 11
01 0B // 11
02 04 12 00 28 09 06 00 08 19 09 00 FF 0F 02 12 00 00
02 04 12 00 28 09 06 00 08 19 09 00 FF 0F 01 12 00 00
02 04 12 00 01 09 06 00 00 60 01 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 00 01 07 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 00 02 07 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 01 01 08 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 01 02 08 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 01 05 08 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 01 06 08 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 01 07 08 00 FF 0F 02 12 00 00
02 04 12 00 03 09 06 01 01 08 08 00 FF 0F 02 12 00 00
09 06 00 08 19 09 00 FF
09 08 34 33 30 39 34 33 35 31
06 00 00 00 0B
06 00 00 00 00
06 00 00 00 10
06 00 00 00 04
06 00 00 00 00
06 00 00 00 08
06 00 00 00 00
06 00 00 00 01
7C 8B
7E

View File

@@ -4,7 +4,8 @@
#include "Arduino.h"
#define EEPROM_SIZE 1024*3
#define EEPROM_CHECK_SUM 95 // Used to check if config is stored. Change if structure changes
#define EEPROM_CHECK_SUM 100 // Used to check if config is stored. Change if structure changes
#define EEPROM_CLEARED_INDICATOR 0xFC
#define EEPROM_CONFIG_ADDRESS 0
#define EEPROM_TEMP_CONFIG_ADDRESS 2048
@@ -29,7 +30,11 @@
struct SystemConfig {
uint8_t boardType;
}; // 1
bool vendorConfigured;
bool userConfigured;
uint8_t dataCollectionConsent; // 0 = unknown, 1 = accepted, 2 = declined
char country[2];
}; // 6
struct WiFiConfig91 {
char ssid[32];
@@ -54,7 +59,10 @@ struct WiFiConfig {
char hostname[32];
bool mdns;
uint8_t power;
}; // 210
uint8_t sleep;
uint8_t mode;
bool autoreboot;
}; // 213
struct MqttConfig86 {
char host[128];
@@ -87,6 +95,23 @@ struct WebConfig {
}; // 129
struct MeterConfig {
uint32_t baud;
uint8_t parity;
bool invert;
uint8_t distributionSystem;
uint8_t mainFuse;
uint8_t productionCapacity;
uint8_t encryptionKey[16];
uint8_t authenticationKey[16];
uint32_t wattageMultiplier;
uint32_t voltageMultiplier;
uint32_t amperageMultiplier;
uint32_t accumulatedMultiplier;
uint8_t source;
uint8_t parser;
}; // 50
struct MeterConfig95 {
uint32_t baud;
uint8_t parity;
bool invert;
@@ -147,6 +172,13 @@ struct DomoticzConfig {
}; // 10
struct NtpConfig {
bool enable;
bool dhcp;
char server[64];
char timezone[32];
}; // 98
struct NtpConfig96 {
bool enable;
bool dhcp;
int16_t offset;
@@ -159,6 +191,7 @@ struct EntsoeConfig {
char area[17];
char currency[4];
uint32_t multiplier;
bool enabled;
}; // 62
struct EnergyAccountingConfig {
@@ -262,13 +295,13 @@ private:
uint8_t tempSensorCount = 0;
TempSensorConfig** tempSensors = NULL;
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.0
bool relocateConfig94(); // 2.1.4
bool relocateConfig94(); // 2.1.0
bool relocateConfig95(); // 2.1.4
bool relocateConfig96(); // 2.1.14
void saveToFs();
bool loadFromFs(uint8_t version);

View File

@@ -0,0 +1,86 @@
#include <Timezone.h>
#define JULY1970 15634800
TimeChangeRule TC_GMT = {"GMT", Last, Sun, Jan, 0, 0};
TimeChangeRule TC_WET = {"WET", Last, Sun, Oct, 2, 0};
TimeChangeRule TC_WEST = {"WEST", Last, Sun, Mar, 1, 60};
TimeChangeRule TC_CET = {"CET", Last, Sun, Oct, 3, 60};
TimeChangeRule TC_CEST = {"CEST", Last, Sun, Mar, 2, 120};
TimeChangeRule TC_EET = {"EET", Last, Sun, Oct, 4, 120};
TimeChangeRule TC_EEST = {"EEST", Last, Sun, Mar, 3, 180};
Timezone GMT = Timezone(TC_GMT);
Timezone WesterEuropean = Timezone(TC_WET, TC_WEST);
Timezone CentralEuropean = Timezone(TC_CET, TC_CEST);
Timezone EasternEuropean = Timezone(TC_EET, TC_EEST);
Timezone* resolveTimezone(char* name) {
if(strncmp_P(name, PSTR("Europe/"), 7) == 0) {
if(strncmp_P(name+7, PSTR("Amsterdam"), 9) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Athens"), 6) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Belfast"), 7) == 0)
return &WesterEuropean;
if(strncmp_P(name+7, PSTR("Berlin"), 6) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Bratislava"), 10) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Brussels"), 8) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Bucharest"), 9) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Budapest"), 8) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Copenhagen"), 10) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Dublin"), 6) == 0)
return &WesterEuropean;
if(strncmp_P(name+7, PSTR("Helsinki"), 8) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Lisbon"), 6) == 0)
return &WesterEuropean;
if(strncmp_P(name+7, PSTR("Ljubljana"), 9) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("London"), 6) == 0)
return &WesterEuropean;
if(strncmp_P(name+7, PSTR("Luxembourg"), 10) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Madrid"), 6) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Malta"), 5) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Nicosia"), 7) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Oslo"), 4) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Paris"), 5) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Podgorica"), 9) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Prague"), 6) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Riga"), 4) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Rome"), 4) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Sofia"), 5) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Stockholm"), 9) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Tallinn"), 7) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Vienna"), 6) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Vilnius"), 7) == 0)
return &EasternEuropean;
if(strncmp_P(name+7, PSTR("Warsaw"), 6) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Zagreb"), 6) == 0)
return &CentralEuropean;
if(strncmp_P(name+7, PSTR("Zurich"), 6) == 0)
return &CentralEuropean;
}
return &GMT;
}

View File

@@ -1,12 +1,18 @@
#include "AmsConfiguration.h"
bool AmsConfiguration::getSystemConfig(SystemConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(EEPROM_SIZE);
uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
if(configVersion == EEPROM_CHECK_SUM || configVersion == EEPROM_CLEARED_INDICATOR) {
EEPROM.get(CONFIG_SYSTEM_START, config);
EEPROM.end();
return true;
} else {
config.boardType = 0xFF;
config.vendorConfigured = false;
config.userConfigured = false;
config.dataCollectionConsent = 0;
strcpy(config.country, "");
return false;
}
}
@@ -24,6 +30,7 @@ bool AmsConfiguration::getWiFiConfig(WiFiConfig& config) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_WIFI_START, config);
EEPROM.end();
if(config.sleep > 2) config.sleep = 1;
return true;
} else {
clearWifi(config);
@@ -33,6 +40,7 @@ bool AmsConfiguration::getWiFiConfig(WiFiConfig& config) {
bool AmsConfiguration::setWiFiConfig(WiFiConfig& config) {
WiFiConfig existing;
if(config.sleep > 2) config.sleep = 1;
if(getWiFiConfig(existing)) {
wifiChanged |= strcmp(config.ssid, existing.ssid) != 0;
wifiChanged |= strcmp(config.psk, existing.psk) != 0;
@@ -45,6 +53,9 @@ bool AmsConfiguration::setWiFiConfig(WiFiConfig& config) {
}
wifiChanged |= strcmp(config.hostname, existing.hostname) != 0;
wifiChanged |= config.power != existing.power;
wifiChanged |= config.sleep != existing.sleep;
wifiChanged |= config.mode != existing.mode;
wifiChanged |= config.autoreboot != existing.autoreboot;
} else {
wifiChanged = true;
}
@@ -63,11 +74,14 @@ void AmsConfiguration::clearWifi(WiFiConfig& config) {
uint16_t chipId;
#if defined(ESP32)
chipId = ESP.getEfuseMac();
config.power = 195;
#else
chipId = ESP.getChipId();
config.power = 205;
#endif
strcpy(config.hostname, (String("ams-") + String(chipId, HEX)).c_str());
config.mdns = true;
config.sleep = 0xFF;
}
void AmsConfiguration::clearWifiIp(WiFiConfig& config) {
@@ -204,8 +218,8 @@ bool AmsConfiguration::setMeterConfig(MeterConfig& config) {
}
void AmsConfiguration::clearMeter(MeterConfig& config) {
config.baud = 2400;
config.parity = 11; // 8E1
config.baud = 0;
config.parity = 0;
config.invert = false;
config.distributionSystem = 0;
config.mainFuse = 0;
@@ -424,9 +438,8 @@ bool AmsConfiguration::setNtpConfig(NtpConfig& config) {
}
}
ntpChanged |= config.dhcp != existing.dhcp;
ntpChanged |= config.offset != existing.offset;
ntpChanged |= config.summerOffset != existing.summerOffset;
ntpChanged |= strcmp(config.server, existing.server) != 0;
ntpChanged |= strcmp(config.timezone, existing.timezone) != 0;
} else {
ntpChanged = true;
}
@@ -448,9 +461,8 @@ void AmsConfiguration::ackNtpChange() {
void AmsConfiguration::clearNtp(NtpConfig& config) {
config.enable = true;
config.dhcp = true;
config.offset = 360;
config.summerOffset = 360;
strcpy(config.server, "pool.ntp.org");
strcpy(config.timezone, "Europe/Oslo");
}
bool AmsConfiguration::getEntsoeConfig(EntsoeConfig& config) {
@@ -474,6 +486,7 @@ bool AmsConfiguration::setEntsoeConfig(EntsoeConfig& config) {
entsoeChanged |= strcmp(config.area, existing.area) != 0;
entsoeChanged |= strcmp(config.currency, existing.currency) != 0;
entsoeChanged |= config.multiplier != existing.multiplier;
entsoeChanged |= config.enabled != existing.enabled;
} else {
entsoeChanged = true;
}
@@ -508,6 +521,7 @@ bool AmsConfiguration::getEnergyAccountingConfig(EnergyAccountingConfig& config)
if(config.thresholds[9] != 255) {
clearEnergyAccountingConfig(config);
}
if(config.hours > 5) config.hours = 5;
return true;
} else {
return false;
@@ -515,6 +529,7 @@ bool AmsConfiguration::getEnergyAccountingConfig(EnergyAccountingConfig& config)
}
bool AmsConfiguration::setEnergyAccountingConfig(EnergyAccountingConfig& config) {
if(config.hours > 5) config.hours = 5;
EnergyAccountingConfig existing;
if(getEnergyAccountingConfig(existing)) {
for(int i = 0; i < 9; i++) {
@@ -523,6 +538,7 @@ bool AmsConfiguration::setEnergyAccountingConfig(EnergyAccountingConfig& config)
}
}
config.thresholds[9] = 255;
energyAccountingChanged |= config.hours != existing.hours;
} else {
energyAccountingChanged = true;
}
@@ -545,6 +561,7 @@ void AmsConfiguration::clearEnergyAccountingConfig(EnergyAccountingConfig& confi
config.thresholds[7] = 100;
config.thresholds[8] = 150;
config.thresholds[9] = 255;
config.hours = 3;
}
bool AmsConfiguration::isEnergyAccountingChanged() {
@@ -558,6 +575,14 @@ void AmsConfiguration::ackEnergyAccountingChange() {
void AmsConfiguration::clear() {
EEPROM.begin(EEPROM_SIZE);
SystemConfig sys;
EEPROM.get(CONFIG_SYSTEM_START, sys);
sys.userConfigured = false;
sys.dataCollectionConsent = 0;
strcpy(sys.country, "");
EEPROM.put(CONFIG_SYSTEM_START, sys);
MeterConfig meter;
clearMeter(meter);
EEPROM.put(CONFIG_METER_START, meter);
@@ -590,7 +615,11 @@ void AmsConfiguration::clear() {
clearEnergyAccountingConfig(eac);
EEPROM.put(CONFIG_ENERGYACCOUNTING_START, eac);
EEPROM.put(EEPROM_CONFIG_ADDRESS, -1);
DebugConfig debug;
clearDebug(debug);
EEPROM.put(CONFIG_DEBUG_START, debug);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CLEARED_INDICATOR);
EEPROM.commit();
EEPROM.end();
}
@@ -609,22 +638,6 @@ bool AmsConfiguration::hasConfig() {
}
} 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()) {
@@ -665,6 +678,22 @@ bool AmsConfiguration::hasConfig() {
configVersion = 0;
return false;
}
case 95:
configVersion = -1; // Prevent loop
if(relocateConfig95()) {
configVersion = 96;
} else {
configVersion = 0;
return false;
}
case 96:
configVersion = -1; // Prevent loop
if(relocateConfig96()) {
configVersion = 100;
} else {
configVersion = 0;
return false;
}
case EEPROM_CHECK_SUM:
return true;
default:
@@ -717,51 +746,6 @@ void AmsConfiguration::saveTempSensors() {
}
}
bool AmsConfiguration::relocateConfig86() {
MqttConfig86 mqtt86;
MqttConfig mqtt;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_MQTT_START_86, mqtt86);
strcpy(mqtt.host, mqtt86.host);
mqtt.port = mqtt86.port;
strcpy(mqtt.clientId, mqtt86.clientId);
strcpy(mqtt.publishTopic, mqtt86.publishTopic);
strcpy(mqtt.subscribeTopic, mqtt86.subscribeTopic);
strcpy(mqtt.username, mqtt86.username);
strcpy(mqtt.password, mqtt86.password);
mqtt.payloadFormat = mqtt86.payloadFormat;
mqtt.ssl = mqtt86.ssl;
EEPROM.put(CONFIG_MQTT_START, mqtt);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 87);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig87() {
MeterConfig87 meter87;
MeterConfig meter;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_METER_START_87, meter87);
if(meter87.type < 5) {
meter.baud = 2400;
meter.parity = meter87.type == 3 || meter87.type == 4 ? 3 : 11;
meter.invert = false;
} else {
meter.baud = 115200;
meter.parity = 3;
meter.invert = meter87.type == 6;
}
meter.distributionSystem = meter87.distributionSystem;
meter.mainFuse = meter87.mainFuse;
meter.productionCapacity = meter87.productionCapacity;
EEPROM.put(CONFIG_METER_START, meter);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 88);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig90() {
EntsoeConfig entsoe;
EEPROM.begin(EEPROM_SIZE);
@@ -842,6 +826,92 @@ bool AmsConfiguration::relocateConfig94() {
return ret;
}
bool AmsConfiguration::relocateConfig95() {
MeterConfig meter;
MeterConfig95 meter95;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_METER_START, meter);
EEPROM.get(CONFIG_METER_START, meter95);
meter.wattageMultiplier = meter95.wattageMultiplier;
meter.voltageMultiplier = meter95.voltageMultiplier;
meter.amperageMultiplier = meter95.amperageMultiplier;
meter.accumulatedMultiplier = meter95.accumulatedMultiplier;
EEPROM.put(CONFIG_METER_START, meter);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 96);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig96() {
EEPROM.begin(EEPROM_SIZE);
SystemConfig sys;
EEPROM.get(CONFIG_SYSTEM_START, sys);
#if defined(ESP8266)
MeterConfig meter;
EEPROM.get(CONFIG_METER_START, meter);
GpioConfig gpio;
EEPROM.get(CONFIG_GPIO_START, gpio);
switch(sys.boardType) {
case 3: // Pow UART0 -- Now Pow-K UART0
case 4: // Pow GPIO12 -- Now Pow-U UART0
case 5: // Pow-K+ -- Now also Pow-K GPIO12
case 7: // Pow-U+ -- Now also Pow-U GPIO12
if(meter.baud == 2400 && meter.parity == 3) { // 3 == 8N1, assuming Pow-K
if(gpio.hanPin == 3) { // UART0
sys.boardType = 3;
} else if(gpio.hanPin == 12) {
sys.boardType = 5;
}
} else { // Assuming Pow-U
if(gpio.hanPin == 3) { // UART0
sys.boardType = 4;
} else if(gpio.hanPin == 12) {
sys.boardType = 7;
}
}
break;
}
#endif
sys.vendorConfigured = true;
sys.userConfigured = true;
sys.dataCollectionConsent = 0;
strcpy(sys.country, "");
EEPROM.put(CONFIG_SYSTEM_START, sys);
WiFiConfig wifi;
EEPROM.get(CONFIG_WIFI_START, wifi);
wifi.mode = 1; // WIFI_STA
wifi.autoreboot = true;
EEPROM.put(CONFIG_WIFI_START, wifi);
NtpConfig ntp;
NtpConfig96 ntp96;
EEPROM.get(CONFIG_NTP_START, ntp96);
ntp.enable = ntp96.enable;
ntp.dhcp = ntp96.dhcp;
if(ntp96.offset == 360 && ntp96.summerOffset == 360) {
strcpy(ntp.timezone, "Europe/Oslo");
} else {
strcpy(ntp.timezone, "GMT");
}
strcpy(ntp.server, ntp96.server);
EEPROM.put(CONFIG_NTP_START, ntp);
EntsoeConfig entsoe;
EEPROM.get(CONFIG_ENTSOE_START, entsoe);
entsoe.enabled = strlen(entsoe.token) > 0;
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 100);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::save() {
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM);
@@ -1045,8 +1115,7 @@ void AmsConfiguration::print(Print* debugger)
debugger->println("--NTP configuration--");
debugger->printf("Enabled: %s\r\n", ntp.enable ? "Yes" : "No");
if(ntp.enable) {
debugger->printf("Offset: %i\r\n", ntp.offset);
debugger->printf("Summer offset: %i\r\n", ntp.summerOffset);
debugger->printf("Timezone: %s\r\n", ntp.timezone);
debugger->printf("Server: %s\r\n", ntp.server);
debugger->printf("DHCP: %s\r\n", ntp.dhcp ? "Yes" : "No");
}
@@ -1057,12 +1126,12 @@ void AmsConfiguration::print(Print* debugger)
EntsoeConfig entsoe;
if(getEntsoeConfig(entsoe)) {
debugger->println("--ENTSO-E configuration--");
debugger->printf("Token: %s\r\n", entsoe.token);
if(strlen(entsoe.token) > 0) {
if(strlen(entsoe.area) > 0) {
debugger->println("--ENTSO-E configuration--");
debugger->printf("Area: %s\r\n", entsoe.area);
debugger->printf("Currency: %s\r\n", entsoe.currency);
debugger->printf("Multiplier: %f\r\n", entsoe.multiplier / 1000.0);
debugger->printf("Token: %s\r\n", entsoe.token);
}
debugger->println("");
delay(10);

View File

@@ -12,6 +12,7 @@ enum AmsType {
AmsTypeIskra = 0x08,
AmsTypeLandis = 0x09,
AmsTypeSagemcom = 0x0A,
AmsTypeLng = 0x0B,
AmsTypeCustom = 0x88,
AmsTypeUnknown = 0xFF
};
@@ -53,6 +54,14 @@ public:
float getL2PowerFactor();
float getL3PowerFactor();
float getL1ActiveImportPower();
float getL2ActiveImportPower();
float getL3ActiveImportPower();
float getL1ActiveExportPower();
float getL2ActiveExportPower();
float getL3ActiveExportPower();
double getActiveImportCounter();
double getReactiveImportCounter();
double getActiveExportCounter();
@@ -61,6 +70,9 @@ public:
bool isThreePhase();
bool isTwoPhase();
int8_t getLastError();
void setLastError(int8_t);
protected:
unsigned long lastUpdateMillis = 0;
unsigned long lastList2 = 0;
@@ -70,9 +82,14 @@ protected:
time_t meterTimestamp = 0;
uint16_t activeImportPower = 0, reactiveImportPower = 0, activeExportPower = 0, reactiveExportPower = 0;
float l1voltage = 0, l2voltage = 0, l3voltage = 0, l1current = 0, l2current = 0, l3current = 0;
float l1activeImportPower = 0, l2activeImportPower = 0, l3activeImportPower = 0;
float l1activeExportPower = 0, l2activeExportPower = 0, l3activeExportPower = 0;
float powerFactor = 0, l1PowerFactor = 0, l2PowerFactor = 0, l3PowerFactor = 0;
double activeImportCounter = 0, reactiveImportCounter = 0, activeExportCounter = 0, reactiveExportCounter = 0;
bool threePhase = false, twoPhase = false, counterEstimated = false;
int8_t lastError = 0x00;
uint8_t lastErrorCount = 0;
};
#endif

View File

@@ -45,6 +45,12 @@ void AmsData::apply(AmsData& other) {
this->l1PowerFactor = other.getL1PowerFactor();
this->l2PowerFactor = other.getL2PowerFactor();
this->l3PowerFactor = other.getL3PowerFactor();
this->l1activeImportPower = other.getL1ActiveImportPower();
this->l2activeImportPower = other.getL2ActiveImportPower();
this->l3activeImportPower = other.getL3ActiveImportPower();
this->l1activeExportPower = other.getL1ActiveExportPower();
this->l2activeExportPower = other.getL2ActiveExportPower();
this->l3activeExportPower = other.getL3ActiveExportPower();
case 3:
this->meterTimestamp = other.getMeterTimestamp();
this->activeImportCounter = other.getActiveImportCounter();
@@ -161,6 +167,30 @@ float AmsData::getL3PowerFactor() {
return this->l3PowerFactor;
}
float AmsData::getL1ActiveImportPower() {
return this->l1activeImportPower;
}
float AmsData::getL2ActiveImportPower() {
return this->l2activeImportPower;
}
float AmsData::getL3ActiveImportPower() {
return this->l3activeImportPower;
}
float AmsData::getL1ActiveExportPower() {
return this->l1activeExportPower;
}
float AmsData::getL2ActiveExportPower() {
return this->l2activeExportPower;
}
float AmsData::getL3ActiveExportPower() {
return this->l3activeExportPower;
}
double AmsData::getActiveImportCounter() {
return this->activeImportCounter;
}
@@ -184,3 +214,16 @@ bool AmsData::isThreePhase() {
bool AmsData::isTwoPhase() {
return this->twoPhase;
}
int8_t AmsData::getLastError() {
return lastErrorCount > 3 ? lastError : 0;
}
void AmsData::setLastError(int8_t lastError) {
this->lastError = lastError;
if(lastError == 0) {
lastErrorCount = 0;
} else {
lastErrorCount++;
}
}

View File

@@ -7,20 +7,20 @@
struct DayDataPoints {
uint8_t version;
int16_t hImport[24];
uint16_t hImport[24];
time_t lastMeterReadTime;
uint32_t activeImport;
uint32_t activeExport;
int16_t hExport[24];
uint16_t hExport[24];
}; // 112 bytes
struct MonthDataPoints {
uint8_t version;
int16_t dImport[31];
uint16_t dImport[31];
time_t lastMeterReadTime;
uint32_t activeImport;
uint32_t activeExport;
int16_t dExport[31];
uint16_t dExport[31];
}; // 141 bytes
class AmsDataStorage {

View File

@@ -21,7 +21,7 @@ bool AmsDataStorage::update(AmsData* data) {
}
time_t now = time(nullptr);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Time is: %lld\n", (int64_t) now);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Time is: %lu\n", (int32_t) now);
if(tz == NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Timezone is missing\n");
return false;
@@ -30,18 +30,18 @@ bool AmsDataStorage::update(AmsData* data) {
if(data->getMeterTimestamp() > BUILD_EPOCH) {
now = data->getMeterTimestamp();
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Using meter timestamp, which is: %lld\n", (int64_t) now);
debugger->printf("(AmsDataStorage) Using meter timestamp, which is: %lu\n", (int32_t) now);
}
} else if(data->getPackageTimestamp() > BUILD_EPOCH) {
now = data->getPackageTimestamp();
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Using package timestamp, which is: %lld\n", (int64_t) now);
debugger->printf("(AmsDataStorage) Using package timestamp, which is: %lu\n", (int32_t) now);
}
}
}
if(now < BUILD_EPOCH) {
if(debugger->isActive(RemoteDebug::VERBOSE)) {
debugger->printf("(AmsDataStorage) Invalid time: %lld\n", (int64_t) now);
debugger->printf("(AmsDataStorage) Invalid time: %lu\n", (int32_t) now);
}
return false;
}
@@ -63,7 +63,7 @@ bool AmsDataStorage::update(AmsData* data) {
return true;
} else {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Last day update: %lld\n", (int64_t) day.lastMeterReadTime);
debugger->printf("(AmsDataStorage) Last day update: %lu\n", (int32_t) day.lastMeterReadTime);
}
tmElements_t last;
breakTime(day.lastMeterReadTime, last);
@@ -86,7 +86,7 @@ bool AmsDataStorage::update(AmsData* data) {
month.lastMeterReadTime = now;
} else {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Last month update: %lld\n", (int64_t) month.lastMeterReadTime);
debugger->printf("(AmsDataStorage) Last month update: %lu\n", (int32_t) month.lastMeterReadTime);
}
tmElements_t last;
breakTime(tz->toLocal(month.lastMeterReadTime), last);
@@ -156,7 +156,7 @@ bool AmsDataStorage::update(AmsData* data) {
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);
debugger->printf("(AmsDataStorage) Estimated usage for hour %u: %.1f - %.1f (%lu)\n", last.Hour, imp, exp, (int32_t) cur);
}
day.activeImport += imp;
@@ -199,7 +199,7 @@ bool AmsDataStorage::update(AmsData* data) {
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);
debugger->printf("(AmsDataStorage) Last month read after resetting to midnight: %lu\n", (int32_t) month.lastMeterReadTime);
}
float hrs = (now - month.lastMeterReadTime) / 3600.0;
@@ -224,7 +224,7 @@ bool AmsDataStorage::update(AmsData* data) {
setDayExport(last.Day, exp);
if(debugger->isActive(RemoteDebug::INFO)) {
debugger->printf("(AmsDataStorage) Estimated usage for day %u: %.1f - %.1f (%lld)\n", last.Day, imp, exp, (int64_t) cur);
debugger->printf("(AmsDataStorage) Estimated usage for day %u: %.1f - %.1f (%lu)\n", last.Day, imp, exp, (int32_t) cur);
}
month.activeImport += imp;
@@ -383,11 +383,11 @@ bool AmsDataStorage::isDayHappy() {
tmElements_t tm, last;
if(now < day.lastMeterReadTime) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Day %lld < %lld\n", (int64_t) now, (int64_t) day.lastMeterReadTime);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Day %lu < %lu\n", (int32_t) now, (int32_t) day.lastMeterReadTime);
return false;
}
if(now-day.lastMeterReadTime > 3600) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Day %lld - %lld > 3600\n", (int64_t) now, (int64_t) day.lastMeterReadTime);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Day %lu - %lu > 3600\n", (int32_t) now, (int32_t) day.lastMeterReadTime);
return false;
}
breakTime(tz->toLocal(now), tm);
@@ -411,11 +411,11 @@ bool AmsDataStorage::isMonthHappy() {
tmElements_t tm, last;
if(now < month.lastMeterReadTime) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Month %lld < %lld\n", (int64_t) now, (int64_t) month.lastMeterReadTime);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Month %lu < %lu\n", (int32_t) now, (int32_t) month.lastMeterReadTime);
return false;
}
if(now-month.lastMeterReadTime > 86400) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Month %lld - %lld > 3600\n", (int64_t) now, (int64_t) month.lastMeterReadTime);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Month %lu - %lu > 3600\n", (int32_t) now, (int32_t) month.lastMeterReadTime);
return false;
}
breakTime(tz->toLocal(now), tm);

View File

@@ -4,7 +4,6 @@ int8_t DSMRParser::parse(uint8_t *buf, DataParserContext &ctx, bool verified) {
uint16_t crcPos = 0;
bool reachedEnd = verified;
uint8_t lastByte = 0x00;
int c = 0;
for(int pos = 0; pos < ctx.length; pos++) {
uint8_t b = *(buf+pos);
if(pos == 0 && b != '/') return DATA_PARSE_BOUNDRY_FLAG_MISSING;

View File

@@ -27,7 +27,7 @@ int8_t GCMParser::parse(uint8_t *d, DataParserContext &ctx) {
memcpy(ctx.system_title, ptr, systemTitleLength);
memcpy(initialization_vector, ctx.system_title, systemTitleLength);
int len;
int len = 0;
int headersize = 2 + systemTitleLength;
ptr += systemTitleLength;
if(((*ptr) & 0xFF) == 0x81) {

View File

@@ -5,8 +5,6 @@
int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
int len;
uint8_t flag = *d;
uint8_t* ptr;
if(ctx.length < 3)
return DATA_PARSE_INCOMPLETE;

View File

@@ -1,7 +1,6 @@
#include "LlcParser.h"
int8_t LLCParser::parse(uint8_t *buf, DataParserContext &ctx) {
LLCHeader* llc = (LLCHeader*) buf;
ctx.length -= 3;
return 3;
}

View File

@@ -5,8 +5,6 @@ int8_t MBUSParser::parse(uint8_t *d, DataParserContext &ctx) {
int headersize = 3;
int footersize = 1;
uint8_t flag = *d;
uint8_t* ptr;
// https://m-bus.com/documentation-wired/06-application-layer

View File

@@ -7,7 +7,7 @@
#include "AmsConfiguration.h"
#include "EnergyAccounting.h"
#include "HwTools.h"
#include "entsoe/EntsoeApi.h"
#include "EntsoeApi.h"
class AmsMqttHandler {
public:
@@ -15,11 +15,12 @@ public:
this->mqtt = mqtt;
this->json = buf;
};
virtual ~AmsMqttHandler() {};
virtual bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
virtual bool publishTemperatures(AmsConfiguration*, HwTools*);
virtual bool publishPrices(EntsoeApi* eapi);
virtual bool publishSystem(HwTools*);
virtual bool publishSystem(HwTools*, EntsoeApi*, EnergyAccounting*);
protected:
MQTTClient* mqtt;

View File

@@ -316,6 +316,7 @@ $(function() {
url: swv.data('url'),
dataType: 'json'
}).done(function(releases) {
var isnew = false;
if(/^v\d{1,2}\.\d{1,2}\.\d{1,2}$/.test(swv.text()) && fwl.length == 0) {
releases.reverse();
var next_patch;
@@ -352,10 +353,13 @@ $(function() {
});
if(next_minor) {
nextVersion = next_minor;
isnew = true;
} else if(next_major) {
nextVersion = next_major;
isnew = true;
} else if(next_patch) {
nextVersion = next_patch;
isnew = true;
}
} else {
nextVersion = releases[0];
@@ -375,9 +379,11 @@ $(function() {
}
});
};
$('#newVersionTag').text(nextVersion.tag_name);
$('#newVersionUrl').prop('href', nextVersion.html_url);
$('#newVersion').removeClass('d-none');
if(isnew) {
$('#newVersionTag').text(nextVersion.tag_name);
$('#newVersionUrl').prop('href', nextVersion.html_url);
$('#newVersion').removeClass('d-none');
}
}
});
}
@@ -478,7 +484,7 @@ var drawDay = function() {
timeout: 30000,
dataType: 'json',
}).done(function(json) {
data = [['Hour', 'Import', { role: 'style' }, { role: 'annotation' }, 'Export', { role: 'style' }]];
data = [['Hour', 'Import', { role: 'style' }, { role: 'annotation' }, 'Export', { role: 'style' }, { role: 'annotation' }]];
var r = 1;
var hour = moment.utc().hours();
var offset = moment().utcOffset()/60;
@@ -486,13 +492,13 @@ var drawDay = function() {
for(var i = hour; i<24; i++) {
var imp = json["i"+zeropad(i)];
var exp = json["e"+zeropad(i)];
data[r++] = [zeropad((i+offset)%24), imp, "opacity: 0.9;", exp == 0 ? imp.toFixed(1) : imp.toFixed(1) + '\n' + -exp.toFixed(1), exp == 0 ? 0 : -exp, "opacity: 0.9;"];
data[r++] = [zeropad((i+offset)%24), imp, "opacity: 0.9;", imp == 0 ? "" : imp.toFixed(1), exp == 0 ? 0 : -exp, "opacity: 0.9;", exp == 0 ? "" : -exp.toFixed(1)];
min = Math.min(0, -exp);
};
for(var i = 0; i < hour; i++) {
var imp = json["i"+zeropad(i)];
var exp = json["e"+zeropad(i)];
data[r++] = [zeropad((i+offset)%24), imp, "opacity: 0.9;", exp == 0 ? imp.toFixed(1) : imp.toFixed(1) + '\n' + -exp.toFixed(1), exp == 0 ? 0 : -exp, "opacity: 0.9;"];
data[r++] = [zeropad((i+offset)%24), imp, "opacity: 0.9;", imp == 0 ? "" : imp.toFixed(1), exp == 0 ? 0 : -exp, "opacity: 0.9;", exp == 0 ? "" : -exp.toFixed(1)];
min = Math.min(0, -exp);
};
ea = google.visualization.arrayToDataTable(data);
@@ -511,7 +517,7 @@ var drawMonth = function() {
timeout: 30000,
dataType: 'json',
}).done(function(json) {
data = [['Hour', 'Import', { role: 'style' }, { role: 'annotation' }, 'Export', { role: 'style' }]];
data = [['Hour', 'Import', { role: 'style' }, { role: 'annotation' }, 'Export', { role: 'style' }, { role: 'annotation' }]];
var r = 1;
var day = moment().date();
var eom = moment().subtract(1, 'months').endOf('month').date();
@@ -519,13 +525,13 @@ var drawMonth = function() {
for(var i = day; i<=eom; i++) {
var imp = json["i"+zeropad(i)];
var exp = json["e"+zeropad(i)];
data[r++] = [zeropad(i), imp, "opacity: 0.9;", exp == 0 ? imp.toFixed(0) : imp.toFixed(0) + '\n' + -exp.toFixed(0), exp == 0 ? 0 : -exp, "opacity: 0.9;"];
data[r++] = [zeropad(i), imp, "opacity: 0.9;", imp == 0 ? "" : imp.toFixed(0), exp == 0 ? 0 : -exp, "opacity: 0.9;", exp == 0 ? "" : -exp.toFixed(0)];
min = Math.min(0, -exp);
}
for(var i = 1; i < day; i++) {
var imp = json["i"+zeropad(i)];
var exp = json["e"+zeropad(i)];
data[r++] = [zeropad(i), imp, "opacity: 0.9;", exp == 0 ? imp.toFixed(0) : imp.toFixed(0) + '\n' + -exp.toFixed(0), exp == 0 ? 0 : -exp, "opacity: 0.9;"];
data[r++] = [zeropad(i), imp, "opacity: 0.9;", imp == 0 ? "" : imp.toFixed(0), exp == 0 ? 0 : -exp, "opacity: 0.9;", exp == 0 ? "" : -exp.toFixed(0)];
min = Math.min(0, -exp);
}
ma = google.visualization.arrayToDataTable(data);
@@ -651,7 +657,7 @@ var fetch = function() {
if(ip) {
var v = parseInt(json.i);
var pct = (v*100)/parseInt(json.im);
var pct = Math.min((v*100)/parseInt(json.im), 100);
var append = "W";
if(v > 1000 && !swatt) {
v = (v/1000).toFixed(1);
@@ -677,7 +683,7 @@ var fetch = function() {
$('.rim').hide();
if(xp) {
var v = parseInt(json.e);
var pct = (v*100)/(om*1000);
var pct = Math.min((v*100)/(om*1000), 100);
var append = "W";
if(v > 1000 && !swatt) {
v = (v/1000).toFixed(1);
@@ -717,21 +723,21 @@ var fetch = function() {
var u1 = parseFloat(json.u1);
t += u1;
c++;
var pct = (Math.max(parseFloat(json.u1)-195.5, 1)*100/69);
var pct = Math.min(Math.max(parseFloat(json.u1)-195.5, 1)*100/69, 100);
arr[r++] = [ds == 1 ? 'L1-L2' : 'L1', u1, "color: " + voltcol(pct) + ";opacity: 0.9;", u1 + "V"];
}
if(json.u2) {
var u2 = parseFloat(json.u2);
t += u2;
c++;
var pct = (Math.max(parseFloat(json.u2)-195.5, 1)*100/69);
var pct = Math.min(Math.max(parseFloat(json.u2)-195.5, 1)*100/69, 100);
arr[r++] = [ds == 1 ? 'L1-L3' : 'L2', u2, "color: " + voltcol(pct) + ";opacity: 0.9;", u2 + "V"];
}
if(json.u3) {
var u3 = parseFloat(json.u3);
t += u3;
c++;
var pct = (Math.max(parseFloat(json.u3)-195.5, 1)*100/69);
var pct = Math.min(Math.max(parseFloat(json.u3)-195.5, 1)*100/69, 100);
arr[r++] = [ds == 1 ? 'L2-L3' : 'L3', u3, "color: " + voltcol(pct) + ";opacity: 0.9;", u3 + "V"];
}
v = t/c;
@@ -756,19 +762,19 @@ var fetch = function() {
if(json.i1 || json.u1) {
var i1 = parseFloat(json.i1);
dA = true;
var pct = (parseFloat(json.i1)/parseInt(json.mf))*100;
var pct = Math.min((parseFloat(json.i1)/parseInt(json.mf))*100, 100);
arr[r++] = ['L1', pct, "color: " + ampcol(pct) + ";opacity: 0.9;", i1 + "A"];
}
if(json.i2 || json.u2) {
var i2 = parseFloat(json.i2);
dA = true;
var pct = (parseFloat(json.i2)/parseInt(json.mf))*100;
var pct = Math.min((parseFloat(json.i2)/parseInt(json.mf))*100, 100);
arr[r++] = ['L2', pct, "color: " + ampcol(pct) + ";opacity: 0.9;", i2 + "A"];
}
if(json.i3 || json.u3) {
var i3 = parseFloat(json.i3);
dA = true;
var pct = (parseFloat(json.i3)/parseInt(json.mf))*100;
var pct = Math.min((parseFloat(json.i3)/parseInt(json.mf))*100, 100);
arr[r++] = ['L3', pct, "color: " + ampcol(pct) + ";opacity: 0.9;", i3 + "A"];
}
if(dA) {
@@ -790,6 +796,12 @@ var fetch = function() {
if(currency) {
$('.sp').show();
}
if(om > 0) {
$('.se').removeClass('d-none');
$('#eache').html(json.ea.h.p.toFixed(2));
$('#eacde').html(json.ea.d.p.toFixed(1));
$('#eacme').html(json.ea.m.p.toFixed(0));
}
}
if(json.me) {
@@ -878,7 +890,7 @@ var fetch = function() {
var upgrade = function() {
if(nextVersion) {
if(confirm("Are you sure you want to perform upgrade to " + nextVersion.tag_name + "?")) {
if(confirm("WARNING: If you have a M-BUS powered device (Pow-U), please keep USB power connected while upgrading.\n\nAre you sure you want to perform upgrade to " + nextVersion.tag_name + "?")) {
$('#loading-indicator').show();
window.location.href="/upgrade?version=" + nextVersion.tag_name;
}

View File

@@ -23,8 +23,8 @@
"v" : %.3f,
"r" : %d,
"t" : %.2f,
"u" : %lu,
"m" : %lu,
"u" : %u,
"m" : %u,
"em" : %d,
"hm" : %d,
"wm" : %d,
@@ -35,19 +35,26 @@
"ds" : %d,
"ea" : {
"x" : %.1f,
"p" : [ %s ],
"t" : %d,
"h" : {
"u" : %.2f,
"c" : %.2f
"c" : %.2f,
"p" : %.2f,
"i" : %.2f
},
"d" : {
"u" : %.2f,
"c" : %.2f
"c" : %.2f,
"p" : %.2f,
"i" : %.2f
},
"m" : {
"u" : %.2f,
"c" : %.2f
"c" : %.2f,
"p" : %.2f,
"i" : %.2f
}
},
"c" : %lu
"c" : %u
}

View File

@@ -1,18 +1,21 @@
<div class="alert alert-danger">
WARNING: Units powered over M-bus must be connected to an external power supply during firmware upload. Failure to do so may cause power-down during upload resulting in non-functioning unit.
WARNING: Units powered by M-BUS (Pow-U) must be connected to an external power supply during firmware upload. Failure to do so may cause power-down during upload resulting in non-functioning unit.
</div>
<div class="alert alert-warning">
Your board is using {chipset} chipset. Only upload firmware designed for this chipset. Failure to do so may result in non-functioning unit.
<span id="fwDownload" style="display: none;"><br/>Download latest firmware file <a id="fwLink" href="#" data-chipset="{chipset}">here</a></span>
</div>
<div class="alert alert-warning">
When using URL, only a valid ESP OTA server response will be accepted.
</div>
<form method="post" enctype="multipart/form-data" class="upload-form">
<div class="my-3 p-3 bg-white rounded shadow">
<div class="row">
<div class="col-lg-6">
<div class="input-group mb-3">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">Upload</span>
<span class="input-group-text">Upload file</span>
</div>
<div class="custom-file">
<input name="file" type="file" class="custom-file-input" id="fileUploadField">
@@ -21,6 +24,19 @@
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6">or</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">Use URL</span>
</div>
<input type="text" name="url" class="form-control"/>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
@@ -28,7 +44,7 @@
<a href="javascript:history.back();" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Upload</button>
<button class="btn btn-primary">Upgrade firmware</button>
</div>
</div>
</form>
</form>

View File

@@ -1,6 +1,5 @@
<div id="newVersion" class="alert alert-info d-none">New version <span id="newVersionTag"></span>!
<a id="newVersionUrl" href="#" target="_blank">view</a>
<span class="d-none ssl-capable"> or <a href="javascript:upgrade();">upgrade</a></span>
<a id="newVersionUrl" href="#" target="_blank">view</a> or <a href="javascript:upgrade();">click here to upgrade</a>
</div>
</main>

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,6 +1,6 @@
<div class="alert alert-warning">!!WARNING!!<br/>Do not change anything here unless you know exactly what you are doing! Changing things here could cause the device to stop responding</div>
<form method="post" action="/save">
<input type="hidden" name="gpioConfig" value="true"/>
<input type="hidden" name="gc" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>GPIO settings</h6>
<div class="d-flex flex-row flex-wrap">
@@ -8,18 +8,18 @@
<div class="input-group-prepend">
<span class="input-group-text">HAN</span>
</div>
<select name="hanPin" class="form-control">
${options.han}
<select name="h" class="form-control">
${h}
</select>
</div>
<div class="m-2 input-group input-group-sm" style="width: 150px;">
<div class="input-group-prepend">
<span class="input-group-text">LED</span>
</div>
<input name="ledPin" type="number" min="2" max="${gpio.max}" class="form-control" value="${config.ledPin}"/>
<input name="l" type="number" min="2" max="${g}" class="form-control" value="${l}"/>
<div class="input-group-append" title="Inverted">
<label class="input-group-text">
<input type="checkbox" name="ledInverted" value="true" ${config.ledInverted}/> inv
<input type="checkbox" name="i" value="true" ${i}/> inv
</label>
</div>
</div>
@@ -27,12 +27,12 @@
<div class="input-group-prepend">
<span class="input-group-text">RGB</span>
</div>
<input name="ledPinRed" type="number" min="2" max="${gpio.max}" class="form-control" value="${config.ledPinRed}"/>
<input name="ledPinGreen" type="number" min="2" max="${gpio.max}" class="form-control" value="${config.ledPinGreen}"/>
<input name="ledPinBlue" type="number" min="2" max="${gpio.max}" class="form-control" value="${config.ledPinBlue}"/>
<input name="r" type="number" min="2" max="${g}" class="form-control" value="${r}"/>
<input name="e" type="number" min="2" max="${g}" class="form-control" value="${e}"/>
<input name="b" type="number" min="2" max="${g}" class="form-control" value="${b}"/>
<div class="input-group-append" title="Inverted">
<label class="input-group-text">
<input type="checkbox" name="ledRgbInverted" value="true" ${config.ledRgbInverted}/> inv
<input type="checkbox" name="n" value="true" ${n}/> inv
</label>
</div>
</div>
@@ -40,31 +40,31 @@
<div class="input-group-prepend">
<span class="input-group-text">AP button</span>
</div>
<input name="apPin" type="number" min="0" max="${gpio.max}" class="form-control" value="${config.apPin}"/>
<input name="a" type="number" min="0" max="${g}" class="form-control" value="${a}"/>
</div>
<div class="m-2 input-group input-group-sm" style="width: 150px;">
<div class="input-group-prepend">
<span class="input-group-text">Temperature</span>
</div>
<input name="tempSensorPin" type="number" min="0" max="${gpio.max}" class="form-control" value="${config.tempSensorPin}"/>
<input name="t" type="number" min="0" max="${g}" class="form-control" value="${t}"/>
</div>
<div class="m-2 input-group input-group-sm" style="width: 150px;">
<div class="input-group-prepend">
<span class="input-group-text">Analog temp</span>
</div>
<input name="tempAnalogSensorPin" type="number" min="0" max="${gpio.max}" class="form-control" value="${config.tempAnalogSensorPin}"/>
<input name="m" type="number" min="0" max="${g}" class="form-control" value="${m}"/>
</div>
<div class="m-2 input-group input-group-sm" style="width: 100px;">
<div class="input-group-prepend">
<span class="input-group-text">Vcc</span>
</div>
<input name="vccPin" type="number" min="0" max="${gpio.max}" class="form-control" value="${config.vccPin}"/>
<input name="v" type="number" min="0" max="${g}" class="form-control" value="${v}"/>
</div>
<div class="m-2 input-group input-group-sm" style="width: 200px;">
<div class="input-group-prepend">
<span class="input-group-text">GND resistor</span>
</div>
<input type="number" min="1" max="1000" step="1" class="form-control" name="vccResistorGnd" value="${config.vccResistorGnd}" />
<input type="number" min="1" max="1000" step="1" class="form-control" name="d" value="${d}" />
<div class="input-group-append" title="Inverted">
<label class="input-group-text">k&ohm;</label>
</div>
@@ -73,7 +73,7 @@
<div class="input-group-prepend">
<span class="input-group-text">Vcc resistor</span>
</div>
<input type="number" min="1" max="1000" step="1" class="form-control" name="vccResistorVcc" value="${config.vccResistorVcc}" />
<input type="number" min="1" max="1000" step="1" class="form-control" name="s" value="${s}" />
<div class="input-group-append" title="Inverted">
<label class="input-group-text">k&ohm;</label>
</div>
@@ -82,19 +82,19 @@
<div class="input-group-prepend">
<span class="input-group-text">Multiplier</span>
</div>
<input type="number" min="0.1" max="10" step="0.01" class="form-control" name="vccMultiplier" value="${config.vccMultiplier}" />
<input type="number" min="0.1" max="10" step="0.01" class="form-control" name="u" value="${u}" />
</div>
<div class="m-2 input-group input-group-sm" style="width: 120px;">
<div class="input-group-prepend">
<span class="input-group-text">Offset</span>
</div>
<input type="number" min="0.0" max="3.5" step="0.01" class="form-control" name="vccOffset" value="${config.vccOffset}" />
<input type="number" min="0.0" max="3.5" step="0.01" class="form-control" name="o" value="${o}" />
</div>
<div class="m-2 input-group input-group-sm" style="width: 130px;">
<div class="input-group-prepend">
<span class="input-group-text">Boot limit</span>
</div>
<input type="number" min="2.5" max="3.5" step="0.1" class="form-control" name="vccBootLimit" value="${config.vccBootLimit}" />
<input type="number" min="2.5" max="3.5" step="0.1" class="form-control" name="c" value="${c}" />
</div>
</div>
</div>

View File

@@ -1,3 +1,4 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
@@ -63,7 +64,7 @@
<a class="dropdown-item" href="/mqtt">MQTT</a>
<a class="dropdown-item" href="/web">Web</a>
<a class="dropdown-item" href="/ntp">NTP</a>
<a class="dropdown-item d-none ssl-capable" href="/entsoe">ENTSO-E API</a>
<a class="dropdown-item" href="/priceapi">Price API</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="https://github.com/gskjold/AmsToMqttBridge/wiki" target="_blank">Documentation</a>
</div>

View File

@@ -113,7 +113,7 @@
<div class="col-xl-12 mb-3">
<div class="bg-white rounded shadow pt-3 pb-3" style="font-size: 14px;">
<strong class="mr-3 ml-3">Real time calculation</strong><br/>
<strong class="mr-3 ml-3">Real time consumption</strong><br/>
<div class="row">
<div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex">
@@ -151,6 +151,35 @@
</div>
</div>
</div>
<strong class="mr-3 ml-3 se d-none">Real time production</strong><br/>
<div class="row se d-none">
<div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex">
<div>Hour</div>
<div class="flex-fill text-right">
<span id="eache"></span> kWh
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex">
<div>Day</div>
<div class="flex-fill text-right">
<span id="eacde"></span> kWh
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex">
<div>Month</div>
<div class="flex-fill text-right">
<span id="eacme"></span> kWh
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6"></div>
</div>
</div>
</div>

View File

@@ -35,6 +35,7 @@
<span class="input-group-text">Baud rate</span>
</div>
<select class="form-control sd" name="b">
<option value="300" {b300}>300</option>
<option value="2400" {b2400}>2400</option>
<option value="4800" {b4800}>4800</option>
<option value="9600" {b9600}>9600</option>

View File

@@ -0,0 +1,86 @@
<form method="post" action="/save">
<input type="hidden" name="ec" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>Price API</h6>
<div class="row">
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Region</span>
</div>
<select name="ea" class="form-control">
<option value="">None</option>
<optgroup label="Norway">
<option value="10YNO-1--------2" {no1}>NO1</option>
<option value="10YNO-2--------T" {no2}>NO2</option>
<option value="10YNO-3--------J" {no3}>NO3</option>
<option value="10YNO-4--------9" {no4}>NO4</option>
<option value="10Y1001A1001A48H" {no5}>NO5</option>
</optgroup>
<optgroup label="Sweden">
<option value="10Y1001A1001A44P" {se1}>SE1</option>
<option value="10Y1001A1001A45N" {se2}>SE2</option>
<option value="10Y1001A1001A46L" {se3}>SE3</option>
<option value="10Y1001A1001A47J" {se4}>SE4</option>
</optgroup>
<optgroup label="Denmark">
<option value="10YDK-1--------W" {dk1}>DK1</option>
<option value="10YDK-2--------M" {dk2}>DK2</option>
</optgroup>
<option value="10YAT-APG------L" {at}>Austria</option>
<option value="10YBE----------2" {be}>Belgium</option>
<option value="10YCZ-CEPS-----N" {cz}>Czech Republic</option>
<option value="10Y1001A1001A39I" {ee}>Estonia</option>
<option value="10YFI-1--------U" {fi}>Finland</option>
<option value="10YFR-RTE------C" {fr}>France</option>
<option value="10Y1001A1001A83F" {de}>Germany</option>
<option value="10YGB----------A" {gb}>Great Britain</option>
<option value="10YLV-1001A00074" {lv}>Latvia</option>
<option value="10YLT-1001A0008Q" {lt}>Lithuania</option>
<option value="10YNL----------L" {nl}>Netherland</option>
<option value="10YPL-AREA-----S" {pl}>Poland</option>
<option value="10YCH-SWISSGRIDZ" {ch}>Switzerland</option>
</select>
</div>
</div>
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Currency</span>
</div>
<select name="ecu" class="form-control">
<option value="NOK" {nok}>NOK</option>
<option value="SEK" {sek}>SEK</option>
<option value="DKK" {dkk}>DKK</option>
<option value="EUR" {eur}>EUR</option>
</select>
</div>
</div>
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Multiplier</span>
</div>
<input name="em" type="number" min="0.001" max="1000" step="0.001" class="form-control" value="{em}"/>
</div>
</div>
<div class="col-xl-4 col-lg-6 col-md-8 {dt}">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">ENTSO-E token</span>
</div>
<input type="text" name="et" class="form-control" value="{et}" placeholder="Optional"/>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

View File

@@ -18,9 +18,10 @@
</li>
</ul>
</header>
<div class="my-3 p-3 bg-white rounded shadow">
<div class="my-3 p-3 bg-white rounded shadow {rs}">
Device is rebooting. You will be redirected to the main page when it is available again.
</div>
<div class="alert alert-warning shadow {us}">Firmware upgrade in progress, DO NOT disconnect power from the device. You will be redirected to the main page when firmware upgrade is complete.</div>
</main>
<script>
var tries = 0;

View File

@@ -26,6 +26,7 @@
<select name="board" class="form-control" required>
<option value=""></option>
<optgroup label="Custom hardware">
<option value="7" ${config.boardType7}>Pow-U+ (ESP32) from amsleser.no</option>
<option value="6" ${config.boardType6}>Pow-P1 from amsleser.no</option>
<option value="5" ${config.boardType5}>Pow-K+ (ESP32) from amsleser.no</option>
<option value="4" ${config.boardType4}>Pow-U or Pow-K from amsleser.no (GPIO12)</option>

View File

@@ -37,7 +37,7 @@
</div>
</div>
<div class="row" id="i">
<div class="col-xl-3 col-lg-4 form-group">
<div class="col-xl-3 col-lg-4 col-md-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">IP</span>
@@ -45,7 +45,7 @@
<input type="text" name="i" class="form-control sip" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{i}"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 form-group">
<div class="col-xl-3 col-lg-4 col-md-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Netmask</span>
@@ -53,7 +53,7 @@
<input type="text" name="sn" class="form-control sip" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{sn}"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 form-group">
<div class="col-xl-3 col-lg-4 col-md-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Gateway</span>
@@ -61,7 +61,7 @@
<input type="text" name="g" class="form-control sip" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{g}"/>
</div>
</div>
<div class="col-xl-4 col-lg-5 form-group">
<div class="col-xl-4 col-lg-5 col-md-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">DNS 1</span>
@@ -69,7 +69,7 @@
<input type="text" name="d1" class="form-control sip" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{d1}"/>
</div>
</div>
<div class="col-xl-4 col-lg-5 form-group">
<div class="col-xl-4 col-lg-5 col-md-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">DNS 2</span>
@@ -89,7 +89,21 @@
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-4 col-sm-6 form-group">
<div class="input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Power saving</span>
</div>
<select name="z" class="form-control">
<option value="255">Default</option>
<option value="0" {z0}>Off</option>
<option value="1" {z1}>Minimum</option>
<option value="2" {z2}>Maximum</option>
</select>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">

1
lib/ClassicUi/include/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
root/*.h

View File

@@ -2,6 +2,7 @@ static const char HEADER_CACHE_CONTROL[] PROGMEM = "Cache-Control";
static const char HEADER_PRAGMA[] PROGMEM = "Pragma";
static const char HEADER_EXPIRES[] PROGMEM = "Expires";
static const char HEADER_AUTHENTICATE[] PROGMEM = "WWW-Authenticate";
static const char HEADER_LOCATION[] PROGMEM = "Location";
static const char CACHE_CONTROL_NO_CACHE[] PROGMEM = "no-cache, no-store, must-revalidate";
static const char CACHE_1HR[] PROGMEM = "public, max-age=3600";

View File

@@ -13,7 +13,7 @@
#include "EnergyAccounting.h"
#include "Uptime.h"
#include "RemoteDebug.h"
#include "entsoe/EntsoeApi.h"
#include "EntsoeApi.h"
#if defined(ESP8266)
#include <ESP8266WiFi.h>
@@ -24,6 +24,7 @@
#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include <HTTPUpdate.h>
#else
#warning "Unsupported board type"
#endif
@@ -58,6 +59,13 @@ private:
bool uploading = false;
File file;
bool performRestart = false;
bool performUpgrade = false;
bool rebootForUpgrade = false;
#if defined(AMS2MQTT_FIRMWARE_URL)
String customFirmwareUrl = AMS2MQTT_FIRMWARE_URL;
#else
String customFirmwareUrl;
#endif
static const uint16_t BufferSize = 2048;
char* buf;
@@ -81,7 +89,7 @@ private:
void configMqttHtml();
void configWebHtml();
void configDomoticzHtml();
void configEntsoeHtml();
void configPriceApiHtml();
void configNtpHtml();
void configGpioHtml();
void configDebugHtml();
@@ -101,6 +109,7 @@ private:
String getSerialSelectOptions(int selected);
void firmwareHtml();
void firmwarePost();
void firmwareUpload();
void firmwareDownload();
void restartHtml();

View File

@@ -25,8 +25,8 @@ except:
print("WARN: Unable to load minifier")
webroot = "web"
srcroot = "src/web/root"
webroot = "lib/ClassicUi/html"
srcroot = "lib/ClassicUi/include/root"
version = os.environ.get('GITHUB_TAG')
if version == None:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
json/*.h

View File

@@ -12,7 +12,7 @@ public:
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*);
bool publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea);
private:
DomoticzConfig config;

View File

@@ -0,0 +1,76 @@
import os
import re
import shutil
import subprocess
try:
from css_html_js_minify import js_minify
except:
from SCons.Script import (
ARGUMENTS,
COMMAND_LINE_TARGETS,
DefaultEnvironment,
)
env = DefaultEnvironment()
env.Execute(
env.VerboseAction(
'$PYTHONEXE -m pip install "css_html_js_minify" ',
"Installing Python dependencies",
)
)
try:
from css_html_js_minify import js_minify
except:
print("WARN: Unable to load minifier")
webroot = "lib/DomoticzMqttHandler/json"
srcroot = "lib/DomoticzMqttHandler/include/json"
version = os.environ.get('GITHUB_TAG')
if version == None:
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)
os.mkdir(srcroot)
else:
os.mkdir(srcroot)
for filename in os.listdir(webroot):
basename = re.sub("[^0-9a-zA-Z]+", "_", filename)
srcfile = webroot + "/" + filename
dstfile = srcroot + "/" + basename + ".h"
varname = basename.upper()
with open(srcfile, encoding="utf-8") as f:
content = f.read().replace("${version}", version)
try:
if (filename.endswith(".js") and filename != 'gaugemeter.js') or filename.endswith(".json"):
content = js_minify(content)
except:
print("WARN: Unable to minify")
with open(dstfile, "w") as dst:
dst.write("static const char ")
dst.write(varname)
dst.write("[] PROGMEM = R\"==\"==(")
dst.write(content)
dst.write(")==\"==\";\n")
dst.write("const int ");
dst.write(varname)
dst.write("_LEN PROGMEM = ");
dst.write(str(len(content)))
dst.write(";");

View File

@@ -1,5 +1,5 @@
#include "DomoticzMqttHandler.h"
#include "web/root/domoticz_json.h"
#include "json/domoticz_json.h"
bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea) {
bool ret = false;
@@ -71,6 +71,6 @@ bool DomoticzMqttHandler::publishPrices(EntsoeApi* eapi) {
return false;
}
bool DomoticzMqttHandler::publishSystem(HwTools* hw) {
bool DomoticzMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea) {
return false;
}

View File

@@ -4,7 +4,7 @@
#include "Arduino.h"
#include "AmsData.h"
#include "AmsDataStorage.h"
#include "entsoe/EntsoeApi.h"
#include "EntsoeApi.h"
struct EnergyAccountingPeak {
uint8_t day;
@@ -12,6 +12,18 @@ struct EnergyAccountingPeak {
};
struct EnergyAccountingData {
uint8_t version;
uint8_t month;
uint16_t costYesterday;
uint16_t costThisMonth;
uint16_t costLastMonth;
uint16_t incomeYesterday;
uint16_t incomeThisMonth;
uint16_t incomeLastMonth;
EnergyAccountingPeak peaks[5];
};
struct EnergyAccountingData4 {
uint8_t version;
uint8_t month;
uint16_t costYesterday;
@@ -42,16 +54,28 @@ public:
bool save();
double getUseThisHour();
double getCostThisHour();
double getUseToday();
double getUseThisMonth();
double getProducedThisHour();
double getProducedToday();
double getProducedThisMonth();
double getCostThisHour();
double getCostToday();
double getCostYesterday();
double getUseThisMonth();
double getCostThisMonth();
uint16_t getCostLastMonth();
double getIncomeThisHour();
double getIncomeToday();
double getIncomeYesterday();
double getIncomeThisMonth();
uint16_t getIncomeLastMonth();
float getMonthMax();
uint8_t getCurrentThreshold();
EnergyAccountingPeak getPeak(uint8_t);
EnergyAccountingData getData();
void setData(EnergyAccountingData&);
@@ -66,6 +90,7 @@ private:
Timezone *tz = NULL;
uint8_t currentHour = 0, currentDay = 0, currentThresholdIdx = 0;
double use, costHour, costDay;
double produce, incomeHour, incomeDay;
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0 };
void calcDayCost();

View File

@@ -42,11 +42,12 @@ bool EnergyAccounting::update(AmsData* amsData) {
if(!init) {
currentHour = local.Hour;
currentDay = local.Day;
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EnergyAccounting) Initializing data at %lld\n", (int64_t) now);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EnergyAccounting) Initializing data at %lu\n", (int32_t) now);
if(!load()) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) Unable to load existing data\n");
data = { 4, local.Month,
0, 0, 0,
data = { 5, local.Month,
0, 0, 0, // Cost
0, 0, 0, // Income
0, 0, // Peak 1
0, 0, // Peak 2
0, 0, // Peak 3
@@ -57,37 +58,47 @@ bool EnergyAccounting::update(AmsData* amsData) {
for(uint8_t i = 0; i < 5; i++) {
debugger->printf("(EnergyAccounting) Peak hour from day %d: %d\n", data.peaks[i].day, data.peaks[i].value*10);
}
debugger->printf("(EnergyAccounting) Loaded cost yesterday: %d, this month: %d, last month: %d\n", data.costYesterday / 10.0, data.costThisMonth, data.costLastMonth);
debugger->printf("(EnergyAccounting) Loaded cost yesterday: %.2f, this month: %d, last month: %d\n", data.costYesterday / 10.0, data.costThisMonth, data.costLastMonth);
debugger->printf("(EnergyAccounting) Loaded income yesterday: %.2f, this month: %d, last month: %d\n", data.incomeYesterday / 10.0, data.incomeThisMonth, data.incomeLastMonth);
}
init = true;
}
if(!initPrice && eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EnergyAccounting) Initializing prices at %lld\n", (int64_t) now);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EnergyAccounting) Initializing prices at %lu\n", (int32_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);
tmElements_t oneHrAgo;
tmElements_t oneHrAgo, oneHrAgoLocal;
breakTime(now-3600, oneHrAgo);
uint16_t val = ds->getHourImport(oneHrAgo.Hour) / 10;
ret |= updateMax(val, local.Day);
breakTime(tz->toLocal(now-3600), oneHrAgoLocal);
ret |= updateMax(val, oneHrAgoLocal.Day);
currentHour = local.Hour; // Need to be defined here so that day cost is correctly calculated
if(local.Hour > 0) {
calcDayCost();
}
use = 0;
produce = 0;
costHour = 0;
currentHour = local.Hour;
incomeHour = 0;
if(local.Day != currentDay) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New day %d\n", local.Day);
data.costYesterday = costDay * 10;
data.costThisMonth += costDay;
costDay = 0;
data.incomeYesterday = incomeDay * 10;
data.incomeThisMonth += incomeDay;
incomeDay = 0;
currentDay = local.Day;
ret = true;
}
@@ -96,6 +107,8 @@ bool EnergyAccounting::update(AmsData* amsData) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New month %d\n", local.Month);
data.costLastMonth = data.costThisMonth;
data.costThisMonth = 0;
data.incomeLastMonth = data.incomeThisMonth;
data.incomeThisMonth = 0;
for(uint8_t i = 0; i < 5; i++) {
data.peaks[i] = { 0, 0 };
}
@@ -106,19 +119,31 @@ bool EnergyAccounting::update(AmsData* amsData) {
}
unsigned long ms = this->lastUpdateMillis > amsData->getLastUpdateMillis() ? 0 : amsData->getLastUpdateMillis() - this->lastUpdateMillis;
float kwh = (amsData->getActiveImportPower() * (((float) ms) / 3600000.0)) / 1000.0;
float kwhi = (amsData->getActiveImportPower() * (((float) ms) / 3600000.0)) / 1000.0;
float kwhe = (amsData->getActiveExportPower() * (((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(kwhi > 0) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) Adding %.4f kWh import\n", kwhi);
use += kwhi;
if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
float price = eapi->getValueForHour(0);
float cost = price * kwh;
float cost = price * kwhi;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) and %.4f %s\n", cost / 100.0, eapi->getCurrency());
costHour += cost;
costDay += cost;
}
}
if(kwhe > 0) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) Adding %.4f kWh export\n", kwhe);
produce += kwhe;
if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
float price = eapi->getValueForHour(0);
float income = price * kwhe;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) and %.4f %s\n", income / 100.0, eapi->getCurrency());
incomeHour += income;
incomeDay += income;
}
}
if(config != NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) calculating threshold, currently at %d\n", currentThresholdIdx);
@@ -135,13 +160,19 @@ void EnergyAccounting::calcDayCost() {
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++) {
if(initPrice) {
costDay = 0;
incomeDay = 0;
}
for(int i = 0; i < currentHour; 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);
wh = ds->getHourExport(utc.Hour);
incomeDay += price * (wh / 1000.0);
}
initPrice = true;
}
@@ -151,23 +182,60 @@ 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;
tmElements_t utc, local;
breakTime(tz->toLocal(now), local);
for(int i = 0; i < local.Hour; i++) {
for(int i = 0; i < currentHour; i++) {
breakTime(now - ((local.Hour - i) * 3600), utc);
ret += ds->getHourImport(utc.Hour) / 1000.0;
}
return ret + getUseThisHour();
}
double EnergyAccounting::getUseThisMonth() {
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return 0;
float ret = 0;
for(int i = 0; i < currentDay; i++) {
ret += ds->getDayImport(i) / 1000.0;
}
return ret + getUseToday();
}
double EnergyAccounting::getProducedThisHour() {
return produce;
}
double EnergyAccounting::getProducedToday() {
float ret = 0.0;
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return 0;
tmElements_t utc;
for(int i = 0; i < currentHour; i++) {
breakTime(now - ((currentHour - i) * 3600), utc);
ret += ds->getHourExport(utc.Hour) / 1000.0;
}
return ret + getProducedThisHour();
}
double EnergyAccounting::getProducedThisMonth() {
time_t now = time(nullptr);
if(now < BUILD_EPOCH) return 0;
float ret = 0;
for(int i = 0; i < currentDay; i++) {
ret += ds->getDayExport(i) / 1000.0;
}
return ret + getProducedToday();
}
double EnergyAccounting::getCostThisHour() {
return costHour;
}
double EnergyAccounting::getCostToday() {
return costDay;
}
@@ -176,21 +244,6 @@ double EnergyAccounting::getCostYesterday() {
return data.costYesterday / 10.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 + getCostToday();
}
@@ -199,6 +252,26 @@ uint16_t EnergyAccounting::getCostLastMonth() {
return data.costLastMonth;
}
double EnergyAccounting::getIncomeThisHour() {
return incomeHour;
}
double EnergyAccounting::getIncomeToday() {
return incomeDay;
}
double EnergyAccounting::getIncomeYesterday() {
return data.incomeYesterday / 10.0;
}
double EnergyAccounting::getIncomeThisMonth() {
return data.incomeThisMonth + getIncomeToday();
}
uint16_t EnergyAccounting::getIncomeLastMonth() {
return data.incomeLastMonth;
}
uint8_t EnergyAccounting::getCurrentThreshold() {
if(config == NULL)
return 0;
@@ -210,7 +283,37 @@ float EnergyAccounting::getMonthMax() {
uint32_t maxHour = 0.0;
bool included[5] = { false, false, false, false, false };
while(count < config->hours) {
for(uint8_t x = 0;x < min((uint8_t) 5, config->hours); x++) {
uint8_t maxIdx = 0;
uint16_t maxVal = 0;
for(uint8_t i = 0; i < 5; i++) {
if(included[i]) continue;
if(data.peaks[i].day == 0) continue;
if(data.peaks[i].value > maxVal) {
maxVal = data.peaks[i].value;
maxIdx = i;
}
}
if(maxVal > 0) {
included[maxIdx] = true;
count++;
}
}
for(uint8_t i = 0; i < 5; i++) {
if(!included[i]) continue;
maxHour += data.peaks[i].value;
}
return maxHour > 0 ? maxHour / count / 100.0 : 0.0;
}
EnergyAccountingPeak EnergyAccounting::getPeak(uint8_t num) {
if(num < 1 || num > 5) return EnergyAccountingPeak({0,0});
uint8_t count = 0;
bool included[5] = { false, false, false, false, false };
for(uint8_t x = 0;x < min((uint8_t) 5, config->hours); x++) {
uint8_t maxIdx = 0;
uint16_t maxVal = 0;
for(uint8_t i = 0; i < 5; i++) {
@@ -220,17 +323,21 @@ float EnergyAccounting::getMonthMax() {
maxIdx = i;
}
}
included[maxIdx] = true;
count++;
}
for(uint8_t i = 0; i < 5; i++) {
if(!included[i]) continue;
if(data.peaks[i].day > 0) {
maxHour += data.peaks[i].value;
if(maxVal > 0) {
included[maxIdx] = true;
count++;
}
}
return maxHour > 0 ? maxHour / count / 100.0 : 0.0;
uint8_t pos = 0;
for(uint8_t i = 0; i < 5; i++) {
if(!included[i]) continue;
pos++;
if(pos == num) {
return data.peaks[i];
}
}
return EnergyAccountingPeak({0,0});
}
bool EnergyAccounting::load() {
@@ -248,13 +355,27 @@ bool EnergyAccounting::load() {
file.readBytes(buf, file.size());
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EnergyAccounting) Data version %d\n", buf[0]);
if(buf[0] == 4) {
if(buf[0] == 5) {
EnergyAccountingData* data = (EnergyAccountingData*) buf;
memcpy(&this->data, data, sizeof(this->data));
ret = true;
} else if(buf[0] == 4) {
EnergyAccountingData4* data = (EnergyAccountingData4*) buf;
this->data = { 5, data->month,
(uint16_t) (data->costYesterday / 10), (uint16_t) (data->costThisMonth / 100), (uint16_t) (data->costLastMonth / 100),
0,0,0, // Income from production
data->peaks[0].day, data->peaks[0].value,
data->peaks[1].day, data->peaks[1].value,
data->peaks[2].day, data->peaks[2].value,
data->peaks[3].day, data->peaks[3].value,
data->peaks[4].day, data->peaks[4].value
};
ret = true;
} else if(buf[0] == 3) {
EnergyAccountingData* data = (EnergyAccountingData*) buf;
this->data = { 4, data->month,
this->data = { 5, data->month,
(uint16_t) (data->costYesterday / 10), (uint16_t) (data->costThisMonth / 100), (uint16_t) (data->costLastMonth / 100),
0,0,0, // Income from production
data->peaks[0].day, data->peaks[0].value,
data->peaks[1].day, data->peaks[1].value,
data->peaks[2].day, data->peaks[2].value,
@@ -263,8 +384,9 @@ bool EnergyAccounting::load() {
};
ret = true;
} else {
data = { 4, 0,
0, 0, 0,
data = { 5, 0,
0, 0, 0, // Cost
0,0,0, // Income from production
0, 0, // Peak 1
0, 0, // Peak 2
0, 0, // Peak 3
@@ -282,7 +404,7 @@ bool EnergyAccounting::load() {
this->data.peaks[b].day = b;
memcpy(&this->data.peaks[b].value, buf+i, 2);
b++;
if(b >= config->hours) break;
if(b >= config->hours || b >= 5) break;
}
ret = true;
} else if(buf[0] == 1) {

View File

@@ -2,6 +2,7 @@
#define _ENTSOEA44PARSER_H
#include "Stream.h"
#include "PricesContainer.h"
#define DOCPOS_SEEK 0
#define DOCPOS_CURRENCY 1
@@ -14,6 +15,7 @@
class EntsoeA44Parser: public Stream {
public:
EntsoeA44Parser();
virtual ~EntsoeA44Parser();
char* getCurrency();
char* getMeasurementUnit();
@@ -25,6 +27,7 @@ public:
void flush();
size_t write(const uint8_t *buffer, size_t size);
size_t write(uint8_t);
void get(PricesContainer*);
private:
char currency[4];

View File

@@ -4,8 +4,8 @@
#include "TimeLib.h"
#include "Timezone.h"
#include "RemoteDebug.h"
#include "EntsoeA44Parser.h"
#include "AmsConfiguration.h"
#include "EntsoeA44Parser.h"
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
@@ -25,12 +25,16 @@ public:
char* getToken();
char* getCurrency();
char* getArea();
float getValueForHour(int8_t);
float getValueForHour(time_t, int8_t);
int16_t getLastError();
private:
RemoteDebug* debugger;
EntsoeConfig* config = NULL;
HTTPClient http;
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)
@@ -38,20 +42,28 @@ private:
uint64_t lastTodayFetch = 0;
uint64_t lastTomorrowFetch = 0;
uint64_t lastCurrencyFetch = 0;
EntsoeA44Parser* today = NULL;
EntsoeA44Parser* tomorrow = NULL;
PricesContainer* today = NULL;
PricesContainer* tomorrow = NULL;
Timezone* tz = NULL;
static const uint16_t BufferSize = 256;
char* buf;
bool hub = false;
uint8_t* key = NULL;
uint8_t* auth = NULL;
float currencyMultiplier = 0;
int16_t lastError = 0;
PricesContainer* fetchPrices(time_t);
bool retrieve(const char* url, Stream* doc);
float getCurrencyMultiplier(const char* from, const char* to);
void printD(String fmt, ...);
void printE(String fmt, ...);
void debugPrint(byte *buffer, int start, int length);
};
#endif

View File

@@ -0,0 +1,8 @@
#ifndef _PRICESCONTAINER_H
#define _PRICESCONTAINER_H
struct PricesContainer {
char currency[4];
char measurementUnit[4];
int32_t points[24];
};
#endif

View File

@@ -22,7 +22,7 @@ void DnbCurrParser::flush() {
}
size_t DnbCurrParser::write(const uint8_t *buffer, size_t size) {
for(int i = 0; i < size; i++) {
for(size_t i = 0; i < size; i++) {
write(buffer[i]);
}
return size;

View File

@@ -5,6 +5,10 @@ EntsoeA44Parser::EntsoeA44Parser() {
for(int i = 0; i < 24; i++) points[i] = ENTSOE_NO_VALUE;
}
EntsoeA44Parser::~EntsoeA44Parser() {
}
char* EntsoeA44Parser::getCurrency() {
return currency;
}
@@ -35,7 +39,7 @@ void EntsoeA44Parser::flush() {
}
size_t EntsoeA44Parser::write(const uint8_t *buffer, size_t size) {
for(int i = 0; i < size; i++) {
for(size_t i = 0; i < size; i++) {
write(buffer[i]);
}
return size;
@@ -102,3 +106,35 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
}
return 1;
}
void EntsoeA44Parser::get(PricesContainer* container) {
strcpy(container->currency, currency);
strcpy(container->measurementUnit, measurementUnit);
container->points[0] = points[0] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[0] * 10000;
container->points[1] = points[1] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[1] * 10000;
container->points[2] = points[2] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[2] * 10000;
container->points[3] = points[3] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[3] * 10000;
container->points[4] = points[4] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[4] * 10000;
container->points[5] = points[5] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[5] * 10000;
container->points[6] = points[6] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[6] * 10000;
container->points[7] = points[7] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[7] * 10000;
container->points[8] = points[8] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[8] * 10000;
container->points[9] = points[9] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[9] * 10000;
container->points[10] = points[10] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[10] * 10000;
container->points[11] = points[11] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[11] * 10000;
container->points[12] = points[12] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[12] * 10000;
container->points[13] = points[13] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[13] * 10000;
container->points[14] = points[14] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[14] * 10000;
container->points[15] = points[15] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[15] * 10000;
container->points[16] = points[16] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[16] * 10000;
container->points[17] = points[17] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[17] * 10000;
container->points[18] = points[18] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[18] * 10000;
container->points[19] = points[19] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[19] * 10000;
container->points[20] = points[20] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[20] * 10000;
container->points[21] = points[21] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[21] * 10000;
container->points[22] = points[22] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[22] * 10000;
container->points[23] = points[23] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[23] * 10000;
}

View File

@@ -5,6 +5,8 @@
#include "DnbCurrParser.h"
#include "version.h"
#include "GcmParser.h"
#if defined(ESP32)
#include <esp_task_wdt.h>
#endif
@@ -31,6 +33,26 @@ void EntsoeApi::setup(EntsoeConfig& config) {
if(today != NULL) delete today;
if(tomorrow != NULL) delete tomorrow;
today = tomorrow = NULL;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setReuse(false);
http.setTimeout(60000);
http.setUserAgent("ams2mqtt/" + String(VERSION));
http.useHTTP10(true);
#if defined(AMS2MQTT_PRICE_KEY)
key = new uint8_t[16] AMS2MQTT_PRICE_KEY;
hub = true;
#else
hub = false;
#endif
#if defined(AMS2MQTT_PRICE_AUTHENTICATION)
auth = new uint8_t[16] AMS2MQTT_PRICE_AUTHENTICATION;
hub = hub && true;
#else
hub = false;
#endif
}
char* EntsoeApi::getToken() {
@@ -41,6 +63,10 @@ char* EntsoeApi::getCurrency() {
return this->config->currency;
}
char* EntsoeApi::getArea() {
return this->config->area;
}
float EntsoeApi::getValueForHour(int8_t hour) {
time_t cur = time(nullptr);
return getValueForHour(cur, hour);
@@ -60,25 +86,33 @@ float EntsoeApi::getValueForHour(time_t cur, int8_t hour) {
if(pos > 23) {
if(tomorrow == NULL)
return ENTSOE_NO_VALUE;
value = tomorrow->getPoint(pos-24);
if(value != ENTSOE_NO_VALUE && strcmp(tomorrow->getMeasurementUnit(), "MWH") == 0) {
if(tomorrow->points[pos-24] == ENTSOE_NO_VALUE)
return ENTSOE_NO_VALUE;
value = tomorrow->points[pos-24] / 10000.0;
if(strcmp(tomorrow->measurementUnit, "KWH") == 0) {
// Multiplier is 1
} else if(strcmp(tomorrow->measurementUnit, "MWH") == 0) {
multiplier *= 0.001;
} else {
return ENTSOE_NO_VALUE;
}
float mult = getCurrencyMultiplier(tomorrow->getCurrency(), config->currency);
float mult = getCurrencyMultiplier(tomorrow->currency, 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(value != ENTSOE_NO_VALUE && strcmp(today->getMeasurementUnit(), "MWH") == 0) {
if(today->points[pos] == ENTSOE_NO_VALUE)
return ENTSOE_NO_VALUE;
value = today->points[pos] / 10000.0;
if(strcmp(today->measurementUnit, "KWH") == 0) {
// Multiplier is 1
} else if(strcmp(today->measurementUnit, "MWH") == 0) {
multiplier *= 0.001;
} else {
return ENTSOE_NO_VALUE;
}
float mult = getCurrencyMultiplier(today->getCurrency(), config->currency);
float mult = getCurrencyMultiplier(today->currency, config->currency);
if(mult == 0) return ENTSOE_NO_VALUE;
multiplier *= mult;
}
@@ -86,15 +120,24 @@ float EntsoeApi::getValueForHour(time_t cur, int8_t hour) {
}
bool EntsoeApi::loop() {
if(strlen(getToken()) == 0)
return false;
uint64_t now = millis64();
if(now < 10000) return false; // Grace period
time_t t = time(nullptr);
if(t < BUILD_EPOCH) return false;
#ifndef AMS2MQTT_PRICE_KEY
if(strlen(getToken()) == 0) {
return false;
}
#endif
if(!config->enabled)
return false;
if(strlen(config->area) == 0)
return false;
if(strlen(config->currency) == 0)
return false;
bool ret = false;
tmElements_t tm;
breakTime(tz->toLocal(t), tm);
@@ -106,7 +149,7 @@ bool EntsoeApi::loop() {
if(midnightMillis == 0) {
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);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Setting midnight millis %llu\n", midnightMillis);
currentDay = tm.Day;
return false;
} else if(now > midnightMillis && currentDay != tm.Day) {
@@ -120,39 +163,15 @@ bool EntsoeApi::loop() {
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 = t - (tm.Hour * 3600) - (tm.Minute * 60) - tm.Second; // UTC midnight
time_t e2 = e1 + SECS_PER_DAY;
tmElements_t d1, d2;
breakTime(tz->toUTC(e1), d1); // To get day and hour for CET/CEST at UTC midnight
breakTime(tz->toUTC(e2), d2);
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, d1.Hour, 00,
d2.Year+1970, d2.Month, d2.Day, d2.Hour, 00,
config->area, config->area);
#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(buf, a44) && a44->getPoint(0) != ENTSOE_NO_VALUE) {
today = a44;
return true;
} else if(a44 != NULL) {
delete a44;
try {
lastTodayFetch = now;
today = fetchPrices(t);
} catch(const std::exception& e) {
if(lastError == 0) lastError = 900;
today = NULL;
return false;
}
return today != NULL;
}
// Prices for next day are published at 13:00 CE(S)T, but to avoid heavy server traffic at that time, we will
@@ -161,49 +180,23 @@ bool EntsoeApi::loop() {
&& midnightMillis - now < tomorrowFetchMillis
&& (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 900000)
) {
lastTomorrowFetch = now;
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(tz->toUTC(e1), d1);
breakTime(tz->toUTC(e2), d2);
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, d1.Hour, 00,
d2.Year+1970, d2.Month, d2.Day, d2.Hour, 00,
config->area, config->area);
#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(buf, a44) && a44->getPoint(0) != ENTSOE_NO_VALUE) {
tomorrow = a44;
return true;
} else if(a44 != NULL) {
delete a44;
try {
breakTime(t+SECS_PER_DAY, tm); // Break UTC tomorrow to find UTC midnight
lastTomorrowFetch = now;
tomorrow = fetchPrices(t+SECS_PER_DAY);
} catch(const std::exception& e) {
if(lastError == 0) lastError = 900;
tomorrow = NULL;
return false;
}
return tomorrow != NULL;
}
}
return ret;
}
bool EntsoeApi::retrieve(const char* url, Stream* doc) {
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)) {
if(http.begin(url)) {
printD("Connection established");
#if defined(ESP32)
@@ -212,7 +205,7 @@ bool EntsoeApi::retrieve(const char* url, Stream* doc) {
ESP.wdtFeed();
#endif
int status = https.GET();
int status = http.GET();
#if defined(ESP32)
esp_task_wdt_reset();
@@ -222,15 +215,17 @@ bool EntsoeApi::retrieve(const char* url, Stream* doc) {
if(status == HTTP_CODE_OK) {
printD("Receiving data");
https.writeToStream(doc);
https.end();
http.writeToStream(doc);
http.end();
lastError = 0;
return true;
} else {
lastError = status;
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("(EntsoeApi) Communication error, returned status: %d\n", status);
printE(https.errorToString(status));
printD(https.getString());
printE(http.errorToString(status));
printD(http.getString());
https.end();
http.end();
return false;
}
} else {
@@ -282,6 +277,108 @@ float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to) {
return currencyMultiplier;
}
PricesContainer* EntsoeApi::fetchPrices(time_t t) {
tmElements_t tm;
breakTime(t, tm);
if(strlen(getToken()) > 0) {
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(tz->toUTC(e1), d1); // To get day and hour for CET/CEST at UTC midnight
breakTime(tz->toUTC(e2), d2);
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, d1.Hour, 00,
d2.Year+1970, d2.Month, d2.Day, d2.Hour, 00,
config->area, config->area);
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Fetching prices for %d.%d.%d\n", tm.Day, tm.Month, tm.Year+1970);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf);
EntsoeA44Parser a44;
if(retrieve(buf, &a44) && a44.getPoint(0) != ENTSOE_NO_VALUE) {
PricesContainer* ret = new PricesContainer();
a44.get(ret);
return ret;
} else {
return NULL;
}
} else if(hub) {
String data;
snprintf(buf, BufferSize, "%s/%s/%d/%d/%d?currency=%s",
"http://ams2mqtt.rewiredinvent.no/hub/price",
config->area,
tm.Year+1970,
tm.Month,
tm.Day,
config->currency
);
#if defined(ESP8266)
WiFiClient client;
client.setTimeout(5000);
if(http.begin(client, buf)) {
#elif defined(ESP32)
if(http.begin(buf)) {
#endif
int status = http.GET();
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
if(status == HTTP_CODE_OK) {
printD("Receiving data");
data = http.getString();
http.end();
lastError = 0;
} else {
lastError = status;
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("(EntsoeApi) Communication error, returned status: %d\n", status);
printE(http.errorToString(status));
printD(http.getString());
http.end();
}
}
uint8_t* content = (uint8_t*) (data.c_str());
if(debugger->isActive(RemoteDebug::DEBUG)) {
printD("Received content for prices:");
debugPrint(content, 0, data.length());
}
DataParserContext ctx;
ctx.length = data.length();
GCMParser gcm(key, auth);
int8_t gcmRet = gcm.parse(content, ctx);
if(debugger->isActive(RemoteDebug::DEBUG)) {
printD("Decrypted content for prices:");
debugPrint(content, 0, data.length());
}
if(gcmRet > 0) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) Price data starting at: %d\n", gcmRet);
PricesContainer* ret = new PricesContainer();
memcpy(ret, content+gcmRet, sizeof(*ret));
for(uint8_t i = 0; i < 24; i++) {
ret->points[i] = ntohl(ret->points[i]);
}
lastError = 0;
return ret;
} else {
lastError = gcmRet;
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("(EntsoeApi) Error code while decrypting prices: %d\n", gcmRet);
}
}
return NULL;
}
void EntsoeApi::printD(String fmt, ...) {
va_list args;
va_start(args, fmt);
@@ -295,3 +392,23 @@ void EntsoeApi::printE(String fmt, ...) {
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args);
va_end(args);
}
void EntsoeApi::debugPrint(byte *buffer, int start, int length) {
for (int i = start; i < start + length; i++) {
if (buffer[i] < 0x10)
debugger->print("0");
debugger->print(buffer[i], HEX);
debugger->print(" ");
if ((i - start + 1) % 16 == 0)
debugger->println("");
else if ((i - start + 1) % 4 == 0)
debugger->print(" ");
yield(); // Let other get some resources too
}
debugger->println("");
}
int16_t EntsoeApi::getLastError() {
return lastError;
}

View File

@@ -0,0 +1 @@
json/*.h

View File

@@ -13,11 +13,9 @@ public:
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*);
bool publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea);
private:
static const uint8_t sensors = 17;
String haTopic = "homeassistant/sensor/";
String haName = "AMS reader";

View File

@@ -0,0 +1,81 @@
#ifndef _HOMEASSISTANTSTATIC_H
#define _HOMEASSISTANTSTATIC_H
#include "Arduino.h"
struct HomeAssistantSensor {
const char* name;
const char* topic;
const char* path;
const char* uom;
const char* devcl;
const char* stacl;
};
const uint8_t HA_SENSOR_COUNT PROGMEM = 60;
HomeAssistantSensor HA_SENSORS[HA_SENSOR_COUNT] PROGMEM = {
{"Status", "/state", "rssi", "dBm", "signal_strength", "\"measurement\""},
{"Supply volt", "/state", "vcc", "V", "voltage", "\"measurement\""},
{"Temperature", "/state", "temp", "C", "temperature", "\"measurement\""},
{"Active import", "/power", "P", "W", "power", "\"measurement\""},
{"L1 active import", "/power", "P1", "W", "power", "\"measurement\""},
{"L2 active import", "/power", "P2", "W", "power", "\"measurement\""},
{"L3 active import", "/power", "P3", "W", "power", "\"measurement\""},
{"Reactive import", "/power", "Q", "VAr", "reactive_power", "\"measurement\""},
{"Active export", "/power", "PO", "W", "power", "\"measurement\""},
{"L1 active export", "/power", "PO1", "W", "power", "\"measurement\""},
{"L2 active export", "/power", "PO2", "W", "power", "\"measurement\""},
{"L3 active export", "/power", "PO3", "W", "power", "\"measurement\""},
{"Reactive export", "/power", "QO", "VAr", "reactive_power", "\"measurement\""},
{"L1 current", "/power", "I1", "A", "current", "\"measurement\""},
{"L2 current", "/power", "I2", "A", "current", "\"measurement\""},
{"L3 current", "/power", "I3", "A", "current", "\"measurement\""},
{"L1 voltage", "/power", "U1", "V", "voltage", "\"measurement\""},
{"L2 voltage", "/power", "U2", "V", "voltage", "\"measurement\""},
{"L3 voltage", "/power", "U3", "V", "voltage", "\"measurement\""},
{"Accumulated active import", "/energy", "tPI", "kWh", "energy", "\"total_increasing\""},
{"Accumulated active export", "/energy", "tPO", "kWh", "energy", "\"total_increasing\""},
{"Accumulated reactive import","/energy", "tQI", "kVArh","energy", "\"total_increasing\""},
{"Accumulated reactive export","/energy", "tQO", "kVArh","energy", "\"total_increasing\""},
{"Power factor", "/power", "PF", "", "power_factor", "\"measurement\""},
{"L1 power factor", "/power", "PF1", "", "power_factor", "\"measurement\""},
{"L2 power factor", "/power", "PF2", "", "power_factor", "\"measurement\""},
{"L3 power factor", "/power", "PF3", "", "power_factor", "\"measurement\""},
{"Price current hour", "/prices", "prices['0']", "", "monetary", ""},
{"Price next hour", "/prices", "prices['1']", "", "monetary", ""},
{"Price in two hour", "/prices", "prices['2']", "", "monetary", ""},
{"Price in three hour", "/prices", "prices['3']", "", "monetary", ""},
{"Price in four hour", "/prices", "prices['4']", "", "monetary", ""},
{"Price in five hour", "/prices", "prices['5']", "", "monetary", ""},
{"Price in six hour", "/prices", "prices['6']", "", "monetary", ""},
{"Price in seven hour", "/prices", "prices['7']", "", "monetary", ""},
{"Price in eight hour", "/prices", "prices['8']", "", "monetary", ""},
{"Price in nine hour", "/prices", "prices['9']", "", "monetary", ""},
{"Price in ten hour", "/prices", "prices['10']", "", "monetary", ""},
{"Price in eleven hour", "/prices", "prices['11']", "", "monetary", ""},
{"Minimum price ahead", "/prices", "prices.min", "", "monetary", ""},
{"Maximum price ahead", "/prices", "prices.max", "", "monetary", ""},
{"Cheapest 1hr period ahead", "/prices", "prices.cheapest1hr","", "timestamp", ""},
{"Cheapest 3hr period ahead", "/prices", "prices.cheapest3hr","", "timestamp", ""},
{"Cheapest 6hr period ahead", "/prices", "prices.cheapest6hr","", "timestamp", ""},
{"Month max", "/realtime","max", "kWh", "energy", "\"total_increasing\""},
{"Tariff threshold", "/realtime","threshold", "kWh", "energy", "\"total_increasing\""},
{"Current hour used", "/realtime","hour.use", "kWh", "energy", "\"total_increasing\""},
{"Current hour cost", "/realtime","hour.cost", "", "monetary", "\"total_increasing\""},
{"Current hour produced", "/realtime","hour.produced", "kWh", "energy", "\"total_increasing\""},
{"Current day used", "/realtime","day.use", "kWh", "energy", "\"total_increasing\""},
{"Current day cost", "/realtime","day.cost", "", "monetary", "\"total_increasing\""},
{"Current day produced", "/realtime","day.produced", "kWh", "energy", "\"total_increasing\""},
{"Current month used", "/realtime","month.use", "kWh", "energy", "\"total_increasing\""},
{"Current month cost", "/realtime","month.cost", "", "monetary", "\"total_increasing\""},
{"Current month produced", "/realtime","month.produced", "kWh", "energy", "\"total_increasing\""},
{"Current month peak 1", "/realtime","peaks[0]", "kWh", "energy", ""},
{"Current month peak 2", "/realtime","peaks[1]", "kWh", "energy", ""},
{"Current month peak 3", "/realtime","peaks[2]", "kWh", "energy", ""},
{"Current month peak 4", "/realtime","peaks[3]", "kWh", "energy", ""},
{"Current month peak 5", "/realtime","peaks[4]", "kWh", "energy", ""},
};
#endif

View File

@@ -3,5 +3,5 @@
"tPO" : %.2f,
"tQI" : %.2f,
"tQO" : %.2f,
"rtc" : %llu
"rtc" : %lu
}

View File

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

View File

@@ -3,8 +3,14 @@
"id" : "%s",
"type" : "%s",
"P" : %d,
"P1" : %.2f,
"P2" : %.2f,
"P3" : %.2f,
"Q" : %d,
"PO" : %d,
"PO1" : %.2f,
"PO2" : %.2f,
"PO3" : %.2f,
"QO" : %d,
"I1" : %.2f,
"I2" : %.2f,

View File

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

View File

@@ -0,0 +1,76 @@
import os
import re
import shutil
import subprocess
try:
from css_html_js_minify import js_minify
except:
from SCons.Script import (
ARGUMENTS,
COMMAND_LINE_TARGETS,
DefaultEnvironment,
)
env = DefaultEnvironment()
env.Execute(
env.VerboseAction(
'$PYTHONEXE -m pip install "css_html_js_minify" ',
"Installing Python dependencies",
)
)
try:
from css_html_js_minify import js_minify
except:
print("WARN: Unable to load minifier")
webroot = "lib/HomeAssistantMqttHandler/json"
srcroot = "lib/HomeAssistantMqttHandler/include/json"
version = os.environ.get('GITHUB_TAG')
if version == None:
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)
os.mkdir(srcroot)
else:
os.mkdir(srcroot)
for filename in os.listdir(webroot):
basename = re.sub("[^0-9a-zA-Z]+", "_", filename)
srcfile = webroot + "/" + filename
dstfile = srcroot + "/" + basename + ".h"
varname = basename.upper()
with open(srcfile, encoding="utf-8") as f:
content = f.read().replace("${version}", version)
try:
if (filename.endswith(".js") and filename != 'gaugemeter.js') or filename.endswith(".json"):
content = js_minify(content)
except:
print("WARN: Unable to minify")
with open(dstfile, "w") as dst:
dst.write("static const char ")
dst.write(varname)
dst.write("[] PROGMEM = R\"==\"==(")
dst.write(content)
dst.write(")==\"==\";\n")
dst.write("const int ");
dst.write(varname)
dst.write("_LEN PROGMEM = ");
dst.write(str(len(content)))
dst.write(";");

View File

@@ -3,12 +3,14 @@
#include "hexutils.h"
#include "Uptime.h"
#include "version.h"
#include "web/root/ha1_json.h"
#include "web/root/ha2_json.h"
#include "web/root/ha3_json.h"
#include "web/root/jsonsys_json.h"
#include "web/root/jsonprices_json.h"
#include "web/root/hadiscover_json.h"
#include "json/ha1_json.h"
#include "json/ha2_json.h"
#include "json/ha3_json.h"
#include "json/ha4_json.h"
#include "json/jsonsys_json.h"
#include "json/jsonprices_json.h"
#include "json/hadiscover_json.h"
#include "json/realtime_json.h"
bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea) {
if(topic.isEmpty() || !mqtt->connected())
@@ -24,6 +26,7 @@ bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, En
data->getMeterTimestamp()
);
mqtt->publish(topic + "/energy", json);
mqtt->loop();
}
String meterModel = data->getMeterModel();
meterModel.replace("\\", "\\\\");
@@ -31,8 +34,8 @@ bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, En
snprintf_P(json, BufferSize, HA1_JSON,
data->getActiveImportPower()
);
return mqtt->publish(topic + "/power", json);
} else if(data->getListType() >= 2) { // publish power counts and volts/amps
mqtt->publish(topic + "/power", json);
} else if(data->getListType() <= 3) { // publish power counts and volts/amps
snprintf_P(json, BufferSize, HA3_JSON,
data->getListId().c_str(),
data->getMeterId().c_str(),
@@ -46,15 +49,62 @@ bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, En
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage()
);
mqtt->publish(topic + "/power", json);
} else if(data->getListType() == 4) { // publish power counts and volts/amps/phase power and PF
snprintf_P(json, BufferSize, HA4_JSON,
data->getListId().c_str(),
data->getMeterId().c_str(),
meterModel.c_str(),
data->getActiveImportPower(),
data->getL1ActiveImportPower(),
data->getL2ActiveImportPower(),
data->getL3ActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getL1ActiveExportPower(),
data->getL2ActiveExportPower(),
data->getL3ActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage(),
data->getPowerFactor() == 0 ? 1 : data->getPowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL1PowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL2PowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL3PowerFactor()
);
return mqtt->publish(topic + "/power", json);
mqtt->publish(topic + "/power", json);
}
return false;
String peaks = "";
uint8_t peakCount = ea->getConfig()->hours;
if(peakCount > 5) peakCount = 5;
for(uint8_t i = 1; i <= peakCount; i++) {
if(!peaks.isEmpty()) peaks += ",";
peaks += String(ea->getPeak(i).value / 100.0, 2);
}
snprintf_P(json, BufferSize, REALTIME_JSON,
ea->getMonthMax(),
peaks.c_str(),
ea->getCurrentThreshold(),
ea->getUseThisHour(),
ea->getCostThisHour(),
ea->getProducedThisHour(),
ea->getUseToday(),
ea->getCostToday(),
ea->getProducedToday(),
ea->getUseThisMonth(),
ea->getCostThisMonth(),
ea->getProducedThisMonth()
);
mqtt->publish(topic + "/realtime", json);
return true;
}
bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
@@ -91,7 +141,7 @@ bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
time_t now = time(nullptr);
float min1hr, min3hr, min6hr;
float min1hr = 0.0, min3hr = 0.0, min6hr = 0.0;
int8_t min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1;
float min = INT16_MAX, max = INT16_MIN;
float values[24];
@@ -141,7 +191,7 @@ bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
}
char ts1hr[21];
char ts1hr[24];
if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
//Serial.printf("1hr: %d %lu\n", min1hrIdx, ts);
@@ -149,7 +199,7 @@ bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
breakTime(ts, tm);
sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts3hr[21];
char ts3hr[24];
if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
//Serial.printf("3hr: %d %lu\n", min3hrIdx, ts);
@@ -157,7 +207,7 @@ bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
breakTime(ts, tm);
sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts6hr[21];
char ts6hr[24];
if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
//Serial.printf("6hr: %d %lu\n", min6hrIdx, ts);
@@ -186,10 +236,10 @@ bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
ts3hr,
ts6hr
);
return mqtt->publish(topic + "/prices", json);
return mqtt->publish(topic + "/prices", json, true, 0);
}
bool HomeAssistantMqttHandler::publishSystem(HwTools* hw) {
bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea) {
if(topic.isEmpty() || !mqtt->connected()){
sequence = 0;
return false;
@@ -202,7 +252,8 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw) {
(uint32_t) (millis64()/1000),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature()
hw->getTemperature(),
VERSION
);
mqtt->publish(topic + "/state", json);
}
@@ -215,27 +266,46 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw) {
#endif
String haUrl = "http://" + haUID + ".local/";
// Could this be necessary? haUID.replace("-", "_");
uint8_t peakCount = ea->getConfig()->hours;
if(peakCount > 5) peakCount = 5;
for(int i=0;i<sensors;i++){
uint8_t peaks = 0;
for(int i=0;i<HA_SENSOR_COUNT;i++) {
HomeAssistantSensor sensor = HA_SENSORS[i];
String uid = String(sensor.path);
uid.replace(".", "");
uid.replace("[", "");
uid.replace("]", "");
uid.replace("'", "");
String uom = String(sensor.uom);
if(strncmp(sensor.devcl, "monetary", 8) == 0) {
if(eapi == NULL) continue;
uom = String(eapi->getCurrency());
}
if(strncmp(sensor.path, "peaks[", 6) == 0) {
if(peaks >= peakCount) continue;
peaks++;
}
snprintf_P(json, BufferSize, HADISCOVER_JSON,
FPSTR(HA_NAMES[i]),
topic.c_str(), FPSTR(HA_TOPICS[i]),
haUID.c_str(), FPSTR(HA_PARAMS[i]),
haUID.c_str(), FPSTR(HA_PARAMS[i]),
FPSTR(HA_UOM[i]),
FPSTR(HA_PARAMS[i]),
FPSTR(HA_DEVCL[i]),
sensor.name,
topic.c_str(), sensor.topic,
haUID.c_str(), uid.c_str(),
haUID.c_str(), uid.c_str(),
uom.c_str(),
sensor.path,
sensor.devcl,
haUID.c_str(),
haName.c_str(),
haModel.c_str(),
VERSION,
haManuf.c_str(),
haUrl.c_str(),
strlen_P(HA_STACL[i]) > 0 ? ", \"stat_cla\" :" : "",
strlen_P(HA_STACL[i]) > 0 ? (char *) FPSTR(HA_STACL[i]) : ""
strlen_P(sensor.stacl) > 0 ? ", \"stat_cla\" :" : "",
strlen_P(sensor.stacl) > 0 ? (char *) FPSTR(sensor.stacl) : ""
);
mqtt->publish(haTopic + haUID + "_" + FPSTR(HA_PARAMS[i]) + "/config", json, true, 0);
mqtt->publish(haTopic + haUID + "_" + uid.c_str() + "/config", json, true, 0);
}
autodiscoverInit = true;
}
if(listType>0) sequence++;

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