Compare commits

...

79 Commits

Author SHA1 Message Date
Gunnar Skjold
fad6ada1e0 Updated release 2023-01-17 19:15:57 +01:00
Gunnar Skjold
fb1d343ee3 Removed debugging 2023-01-17 19:03:07 +01:00
Gunnar Skjold
1f3c32e80a Adding secrets to build 2023-01-17 18:57:11 +01:00
Gunnar Skjold
43fbca7099 Update release build 2023-01-17 18:42:09 +01:00
Gunnar Skjold
7a36082564 Trying stuff to get build working 2023-01-17 18:34:47 +01:00
Gunnar Skjold
fb2cfdfe01 Trying to fix build 2023-01-17 18:28:08 +01:00
Gunnar Skjold
6c3dca9344 Updated build 2023-01-17 18:15:32 +01:00
Gunnar Skjold
cce5d75fd7 Updated build 2023-01-17 18:13:54 +01:00
Gunnar Skjold
8fd411c1d6 Updated build 2023-01-17 18:13:20 +01:00
Gunnar Skjold
508b2e6c45 Icon for safari 2023-01-17 17:51:50 +01:00
Gunnar Skjold
af630615db Changes after testing 2023-01-17 17:49:01 +01:00
Gunnar Skjold
222a4f13e2 Changes after testing 2023-01-17 17:48:42 +01:00
Gunnar Skjold
3bc6c75c5a Some more changes for L&G 2023-01-15 11:20:28 +01:00
Gunnar Skjold
dbb3eac709 Some changes for L&G 2023-01-15 10:56:46 +01:00
Gunnar Skjold
1cee48eab4 Some design changes 2023-01-14 09:34:28 +01:00
Gunnar Skjold
c28752a00a Fixed firmware upgrade 2023-01-12 21:24:36 +01:00
Gunnar Skjold
365061df29 Some changes after testing 2023-01-12 20:17:37 +01:00
Gunnar Skjold
870617f780 More changes for v2.2 2023-01-11 20:41:27 +01:00
Gunnar Skjold
4972b980ba Changes for v2.2 2023-01-06 14:34:07 +01:00
Gunnar Skjold
51c70abda3 Merge branch 'master' into dev-v2.2 2023-01-04 18:55:38 +01:00
Gunnar Skjold
d49b7eac09 Minor change 2022-12-19 16:49:40 +01:00
Gunnar Skjold
461d76e651 Merge branch 'master' into dev-v2.2 2022-12-15 18:32:36 +01:00
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
2218ac4e8a Fixed graph clearing problem 2022-12-07 11:32:55 +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
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
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
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
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
root
feffbb53a3 Merge branch 'master' of github.com:gskjold/AmsToMqttBridge into hub-prices 2022-10-17 15:42:23 +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
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
root
dc25c147b9 Merge remote-tracking branch 'public/master' into hub-prices 2022-10-07 17:36:49 +00:00
Gunnar Skjold
6a76144566 Merge branch 'master' into hub-prices 2022-10-07 17:49:47 +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
608470c5f7 Merge branch 'master' into hub-prices 2022-08-18 19:56:31 +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
1a92cd1978 Fetch prices from hub 2022-08-12 20:41:53 +02:00
201 changed files with 12538 additions and 812 deletions

View File

@@ -8,6 +8,7 @@ on:
- scripts/**
- web/**
- platformio.ini
- .github/workflows/**
branches:
- '*'
tags:
@@ -22,6 +23,12 @@ jobs:
steps:
- name: Check out code from repo
uses: actions/checkout@v1
- name: Inject secrets into ini file
run: |
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
- name: Cache Python dependencies
uses: actions/cache@v1
with:
@@ -40,6 +47,18 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -U platformio css_html_js_minify
- name: Set up node
uses: actions/setup-node@v1
with:
node-version: '16.x'
- name: Build with node
run: |
cd lib/SvelteUi/app
npm ci
npm run build
cd -
env:
CI: true
- name: PlatformIO lib install
run: pio lib install
- name: PlatformIO run

View File

@@ -23,6 +23,12 @@ jobs:
env:
GITHUB_REF: ${{ github.ref }}
run: echo "GITHUB_TAG=$(echo ${GITHUB_REF##*/})" >> $GITHUB_ENV
- name: Inject secrets into ini file
run: |
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
- name: Cache Python dependencies
uses: actions/cache@v1
with:
@@ -41,12 +47,23 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -U platformio css_html_js_minify
- name: Set up node
uses: actions/setup-node@v1
with:
node-version: '16.x'
- name: Build with node
run: |
cd lib/SvelteUi/app
npm ci
npm run build
cd -
env:
CI: false
- name: PlatformIO lib install
run: pio lib install
- name: PlatformIO run
run: pio run
- name: Create zip files
run: /bin/sh scripts/mkzip.sh
- name: Create Release
id: create_release
uses: actions/create-release@v1.0.0
@@ -58,6 +75,19 @@ jobs:
draft: false
prerelease: false
- name: Build esp8266 firmware
run: pio run -e esp8266
- name: Create esp8266 zip file
run: /bin/sh scripts/esp8266/mkzip.sh
- name: Upload esp8266 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp8266/firmware.bin
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp8266 zip to release
uses: actions/upload-release-asset@v1
env:
@@ -67,6 +97,20 @@ jobs:
asset_path: esp8266.zip
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32 firmware
run: pio run -e esp32
- name: Create esp32 zip file
run: /bin/sh scripts/esp32/mkzip.sh
- name: Upload esp32 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp32/firmware.bin
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32 zip to release
uses: actions/upload-release-asset@v1
env:
@@ -76,6 +120,20 @@ jobs:
asset_path: esp32.zip
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32s2 firmware
run: pio run -e esp32s2
- name: Create esp32s2 zip file
run: /bin/sh scripts/esp32s2/mkzip.sh
- name: Upload esp32s2 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp32s2/firmware.bin
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32s2 zip to release
uses: actions/upload-release-asset@v1
env:
@@ -86,24 +144,10 @@ jobs:
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Upload esp8266 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp8266/firmware.bin
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32 binary to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp32/firmware.bin
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Build esp32solo firmware
run: pio run -e esp32solo
- name: Create esp32solo zip file
run: /bin/sh scripts/esp32solo/mkzip.sh
- name: Upload esp32solo binary to release
uses: actions/upload-release-asset@v1
env:
@@ -113,12 +157,12 @@ jobs:
asset_path: .pio/build/esp32solo/firmware.bin
asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32s2 binary to release
- name: Upload esp32solo zip to release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp32s2/firmware.bin
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
asset_path: esp32solo.zip
asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip

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

@@ -4,12 +4,14 @@
#include "Arduino.h"
#define EEPROM_SIZE 1024*3
#define EEPROM_CHECK_SUM 96 // Used to check if config is stored. Change if structure changes
#define EEPROM_CHECK_SUM 101 // Used to check if config is stored. Change if structure changes
#define EEPROM_CLEARED_INDICATOR 0xFC
#define EEPROM_CONFIG_ADDRESS 0
#define EEPROM_TEMP_CONFIG_ADDRESS 2048
#define CONFIG_SYSTEM_START 8
#define CONFIG_METER_START 32
#define CONFIG_UI_START 248
#define CONFIG_GPIO_START 266
#define CONFIG_ENTSOE_START 290
#define CONFIG_WIFI_START 360
@@ -29,7 +31,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];
@@ -55,7 +61,9 @@ struct WiFiConfig {
bool mdns;
uint8_t power;
uint8_t sleep;
}; // 211
uint8_t mode;
bool autoreboot;
}; // 213
struct MqttConfig86 {
char host[128];
@@ -88,6 +96,23 @@ struct WebConfig {
}; // 129
struct MeterConfig {
uint32_t baud;
uint8_t parity;
bool invert;
uint8_t distributionSystem;
uint16_t mainFuse;
uint16_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;
}; // 52
struct MeterConfig100 {
uint32_t baud;
uint8_t parity;
bool invert;
@@ -165,6 +190,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;
@@ -177,6 +209,7 @@ struct EntsoeConfig {
char area[17];
char currency[4];
uint32_t multiplier;
bool enabled;
}; // 62
struct EnergyAccountingConfig {
@@ -184,6 +217,20 @@ struct EnergyAccountingConfig {
uint8_t hours;
}; // 11
struct UiConfig {
uint8_t showImport;
uint8_t showExport;
uint8_t showVoltage;
uint8_t showAmperage;
uint8_t showReactive;
uint8_t showRealtime;
uint8_t showPeaks;
uint8_t showPricePlot;
uint8_t showDayPlot;
uint8_t showMonthPlot;
uint8_t showTemperaturePlot;
}; // 11
struct TempSensorConfig {
uint8_t address[8];
char name[16];
@@ -260,6 +307,10 @@ public:
bool isEnergyAccountingChanged();
void ackEnergyAccountingChange();
bool getUiConfig(UiConfig&);
bool setUiConfig(UiConfig&);
void clearUiConfig(UiConfig&);
void loadTempSensors();
void saveTempSensors();
uint8_t getTempSensorCount();
@@ -280,14 +331,14 @@ 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 relocateConfig95(); // 2.1.13
bool relocateConfig94(); // 2.1.0
bool relocateConfig95(); // 2.1.4
bool relocateConfig96(); // 2.1.14
bool relocateConfig100(); // 2.2-dev
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;
}
}
@@ -48,6 +54,8 @@ 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;
}
@@ -210,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;
@@ -222,6 +230,8 @@ void AmsConfiguration::clearMeter(MeterConfig& config) {
config.voltageMultiplier = 0;
config.amperageMultiplier = 0;
config.accumulatedMultiplier = 0;
config.source = 1; // Serial
config.parser = 0; // Auto
}
bool AmsConfiguration::isMeterChanged() {
@@ -430,9 +440,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;
}
@@ -454,9 +463,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) {
@@ -480,6 +488,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;
}
@@ -540,7 +549,6 @@ bool AmsConfiguration::setEnergyAccountingConfig(EnergyAccountingConfig& config)
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
void AmsConfiguration::clearEnergyAccountingConfig(EnergyAccountingConfig& config) {
@@ -565,9 +573,53 @@ void AmsConfiguration::ackEnergyAccountingChange() {
energyAccountingChanged = false;
}
bool AmsConfiguration::getUiConfig(UiConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_UI_START, config);
if(config.showImport > 2) clearUiConfig(config); // Must be wrong
EEPROM.end();
return true;
} else {
clearUiConfig(config);
return false;
}
}
bool AmsConfiguration::setUiConfig(UiConfig& config) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_UI_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
void AmsConfiguration::clearUiConfig(UiConfig& config) {
// 1 = Always, 2 = If value present, 0 = Hidden
config.showImport = 1;
config.showExport = 2;
config.showVoltage = 2;
config.showAmperage = 2;
config.showReactive = 0;
config.showRealtime = 1;
config.showPeaks = 2;
config.showPricePlot = 2;
config.showDayPlot = 1;
config.showMonthPlot = 1;
config.showTemperaturePlot = 2;
}
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);
@@ -600,7 +652,15 @@ 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);
UiConfig ui;
clearUiConfig(ui);
EEPROM.put(CONFIG_UI_START, ui);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CLEARED_INDICATOR);
EEPROM.commit();
EEPROM.end();
}
@@ -619,22 +679,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()) {
@@ -683,6 +727,22 @@ bool AmsConfiguration::hasConfig() {
configVersion = 0;
return false;
}
case 96:
configVersion = -1; // Prevent loop
if(relocateConfig96()) {
configVersion = 100;
} else {
configVersion = 0;
return false;
}
case 100:
configVersion = -1; // Prevent loop
if(relocateConfig100()) {
configVersion = 101;
} else {
configVersion = 0;
return false;
}
case EEPROM_CHECK_SUM:
return true;
default:
@@ -735,51 +795,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 = {0,0,0,0,0,0,0};
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);
@@ -877,6 +892,112 @@ bool AmsConfiguration::relocateConfig95() {
return ret;
}
bool AmsConfiguration::relocateConfig96() {
EEPROM.begin(EEPROM_SIZE);
SystemConfig sys;
EEPROM.get(CONFIG_SYSTEM_START, sys);
MeterConfig meter;
EEPROM.get(CONFIG_METER_START, meter);
meter.source = 1; // Serial
meter.parser = 0; // Auto
EEPROM.put(CONFIG_METER_START, meter);
#if defined(ESP8266)
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::relocateConfig100() {
EEPROM.begin(EEPROM_SIZE);
MeterConfig100 meter100;
EEPROM.get(CONFIG_METER_START, meter100);
MeterConfig meter;
meter.baud = meter100.baud;
meter.parity = meter100.parity;
meter.invert = meter100.invert;
meter.distributionSystem = meter100.distributionSystem;
meter.mainFuse = meter100.mainFuse;
meter.productionCapacity = meter100.productionCapacity;
memcpy(meter.encryptionKey, meter100.encryptionKey, 16);
memcpy(meter.authenticationKey, meter100.authenticationKey, 16);
meter.wattageMultiplier = meter100.wattageMultiplier;
meter.voltageMultiplier = meter100.voltageMultiplier;
meter.amperageMultiplier = meter100.amperageMultiplier;
meter.accumulatedMultiplier = meter100.accumulatedMultiplier;
meter.source = meter100.source;
meter.parser = meter100.parser;
EEPROM.put(CONFIG_METER_START, meter);
UiConfig ui;
clearUiConfig(ui);
EEPROM.put(CONFIG_UI_START, ui);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 101);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::save() {
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM);
@@ -1080,8 +1201,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");
}
@@ -1092,12 +1212,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,7 +12,6 @@ enum AmsType {
AmsTypeIskra = 0x08,
AmsTypeLandisGyr = 0x09,
AmsTypeSagemcom = 0x0A,
AmsTypeLng = 0x0B,
AmsTypeCustom = 0x88,
AmsTypeUnknown = 0xFF
};
@@ -70,6 +69,9 @@ public:
bool isThreePhase();
bool isTwoPhase();
int8_t getLastError();
void setLastError(int8_t);
protected:
unsigned long lastUpdateMillis = 0;
unsigned long lastList2 = 0;
@@ -84,6 +86,9 @@ protected:
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

@@ -7,7 +7,7 @@
#include "AmsConfiguration.h"
#include "EnergyAccounting.h"
#include "HwTools.h"
#include "entsoe/EntsoeApi.h"
#include "EntsoeApi.h"
class AmsMqttHandler {
public:

View File

@@ -64,7 +64,6 @@ void AmsData::apply(AmsData& other) {
this->meterType = other.getMeterType();
this->meterModel = other.getMeterModel();
this->reactiveImportPower = other.getReactiveImportPower();
this->activeExportPower = other.getActiveExportPower();
this->reactiveExportPower = other.getReactiveExportPower();
this->l1current = other.getL1Current();
this->l2current = other.getL2Current();
@@ -74,9 +73,13 @@ void AmsData::apply(AmsData& other) {
this->l3voltage = other.getL3Voltage();
this->threePhase = other.isThreePhase();
this->twoPhase = other.isTwoPhase();
case 1:
this->activeImportPower = other.getActiveImportPower();
}
// Moved outside switch to handle meters alternating between sending active and accumulated values
if(other.getListType() == 1 || (other.getActiveImportPower() > 0 || other.getActiveExportPower() > 0))
this->activeImportPower = other.getActiveImportPower();
if(other.getListType() == 2 || (other.getActiveImportPower() > 0 || other.getActiveExportPower() > 0))
this->activeExportPower = other.getActiveExportPower();
}
unsigned long AmsData::getLastUpdateMillis() {
@@ -214,3 +217,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

@@ -12,7 +12,8 @@ struct DayDataPoints {
uint32_t activeImport;
uint32_t activeExport;
uint16_t hExport[24];
}; // 112 bytes
uint8_t accuracy;
}; // 113 bytes
struct MonthDataPoints {
uint8_t version;
@@ -21,17 +22,18 @@ struct MonthDataPoints {
uint32_t activeImport;
uint32_t activeExport;
uint16_t dExport[31];
}; // 141 bytes
uint8_t accuracy;
}; // 142 bytes
class AmsDataStorage {
public:
AmsDataStorage(RemoteDebug*);
void setTimezone(Timezone*);
bool update(AmsData*);
int32_t getHourImport(uint8_t);
int32_t getHourExport(uint8_t);
int32_t getDayImport(uint8_t);
int32_t getDayExport(uint8_t);
uint32_t getHourImport(uint8_t);
uint32_t getHourExport(uint8_t);
uint32_t getDayImport(uint8_t);
uint32_t getDayExport(uint8_t);
bool load();
bool save();
@@ -40,6 +42,11 @@ public:
MonthDataPoints getMonthData();
bool setMonthData(MonthDataPoints&);
uint8_t getDayAccuracy();
void setDayAccuracy(uint8_t);
uint8_t getMonthAccuracy();
void setMonthAccuracy(uint8_t);
bool isHappy();
bool isDayHappy();
bool isMonthHappy();
@@ -50,19 +57,21 @@ private:
0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
10
};
MonthDataPoints month = {
0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
10
};
RemoteDebug* debugger;
void setHourImport(uint8_t, int32_t);
void setHourExport(uint8_t, int32_t);
void setDayImport(uint8_t, int32_t);
void setDayExport(uint8_t, int32_t);
void setHourImport(uint8_t, uint32_t);
void setHourExport(uint8_t, uint32_t);
void setDayImport(uint8_t, uint32_t);
void setDayExport(uint8_t, uint32_t);
};
#endif

View File

@@ -5,8 +5,10 @@
#include "version.h"
AmsDataStorage::AmsDataStorage(RemoteDebug* debugger) {
day.version = 4;
month.version = 5;
day.version = 5;
day.accuracy = 1;
month.version = 6;
month.accuracy = 1;
this->debugger = debugger;
}
@@ -237,44 +239,160 @@ bool AmsDataStorage::update(AmsData* data) {
return ret;
}
void AmsDataStorage::setHourImport(uint8_t hour, int32_t val) {
void AmsDataStorage::setHourImport(uint8_t hour, uint32_t val) {
if(hour < 0 || hour > 24) return;
day.hImport[hour] = val / 10;
uint8_t accuracy = day.accuracy;
uint32_t update = val / pow(10, accuracy);
while(update > UINT16_MAX) {
accuracy++;
update = val / pow(10, accuracy);
}
if(accuracy != day.accuracy) {
setDayAccuracy(accuracy);
}
day.hImport[hour] = update;
uint32_t max = 0;
for(uint8_t i = 0; i < 24; i++) {
if(day.hImport[i] > max)
max = day.hImport[i];
if(day.hExport[i] > max)
max = day.hExport[i];
}
while(max < UINT16_MAX/10 && accuracy > 0) {
accuracy--;
max = max*10;
}
if(accuracy != day.accuracy) {
setDayAccuracy(accuracy);
}
}
int32_t AmsDataStorage::getHourImport(uint8_t hour) {
uint32_t AmsDataStorage::getHourImport(uint8_t hour) {
if(hour < 0 || hour > 24) return 0;
return day.hImport[hour] * 10;
return day.hImport[hour] * pow(10, day.accuracy);
}
void AmsDataStorage::setHourExport(uint8_t hour, int32_t val) {
void AmsDataStorage::setHourExport(uint8_t hour, uint32_t val) {
if(hour < 0 || hour > 24) return;
day.hExport[hour] = val / 10;
uint8_t accuracy = day.accuracy;
uint32_t update = val / pow(10, accuracy);
while(update > UINT16_MAX) {
accuracy++;
update = val / pow(10, accuracy);
}
if(accuracy != day.accuracy) {
setDayAccuracy(accuracy);
}
day.hExport[hour] = update;
uint32_t max = 0;
for(uint8_t i = 0; i < 24; i++) {
if(day.hImport[i] > max)
max = day.hImport[i];
if(day.hExport[i] > max)
max = day.hExport[i];
}
while(max < UINT16_MAX/10 && accuracy > 0) {
accuracy--;
max = max*10;
}
if(accuracy != day.accuracy) {
setDayAccuracy(accuracy);
}
}
int32_t AmsDataStorage::getHourExport(uint8_t hour) {
uint32_t AmsDataStorage::getHourExport(uint8_t hour) {
if(hour < 0 || hour > 24) return 0;
return day.hExport[hour] * 10;
return day.hExport[hour] * pow(10, day.accuracy);
}
void AmsDataStorage::setDayImport(uint8_t day, int32_t val) {
void AmsDataStorage::setDayImport(uint8_t day, uint32_t val) {
if(day < 1 || day > 31) return;
month.dImport[day-1] = val / 10;
uint8_t accuracy = month.accuracy;
uint32_t update = val / pow(10, accuracy);
while(update > UINT16_MAX) {
accuracy++;
update = val / pow(10, accuracy);
}
if(accuracy != month.accuracy) {
setMonthAccuracy(accuracy);
}
month.dImport[day-1] = update;
uint32_t max = 0;
for(uint8_t i = 0; i < 31; i++) {
if(month.dImport[i] > max)
max = month.dImport[i];
if(month.dExport[i] > max)
max = month.dExport[i];
}
while(max < UINT16_MAX/10 && accuracy > 0) {
accuracy--;
max = max*10;
}
if(accuracy != month.accuracy) {
setMonthAccuracy(accuracy);
}
}
int32_t AmsDataStorage::getDayImport(uint8_t day) {
uint32_t AmsDataStorage::getDayImport(uint8_t day) {
if(day < 1 || day > 31) return 0;
return (month.dImport[day-1] * 10);
return (month.dImport[day-1] * pow(10, month.accuracy));
}
void AmsDataStorage::setDayExport(uint8_t day, int32_t val) {
void AmsDataStorage::setDayExport(uint8_t day, uint32_t val) {
if(day < 1 || day > 31) return;
month.dExport[day-1] = val / 10;
uint8_t accuracy = month.accuracy;
uint32_t update = val / pow(10, accuracy);
while(update > UINT16_MAX) {
accuracy++;
update = val / pow(10, accuracy);
}
if(accuracy != month.accuracy) {
setMonthAccuracy(accuracy);
}
month.dExport[day-1] = update;
uint32_t max = 0;
for(uint8_t i = 0; i < 31; i++) {
if(month.dImport[i] > max)
max = month.dImport[i];
if(month.dExport[i] > max)
max = month.dExport[i];
}
while(max < UINT16_MAX/10 && accuracy > 0) {
accuracy--;
max = max*10;
}
if(accuracy != month.accuracy) {
setMonthAccuracy(accuracy);
}
}
int32_t AmsDataStorage::getDayExport(uint8_t day) {
uint32_t AmsDataStorage::getDayExport(uint8_t day) {
if(day < 1 || day > 31) return 0;
return (month.dExport[day-1] * 10);
return (month.dExport[day-1] * pow(10, month.accuracy));
}
bool AmsDataStorage::load() {
@@ -348,31 +466,74 @@ MonthDataPoints AmsDataStorage::getMonthData() {
}
bool AmsDataStorage::setDayData(DayDataPoints& day) {
if(day.version == 4) {
if(day.version == 5) {
this->day = day;
return true;
} else if(day.version == 4) {
this->day = day;
this->day.accuracy = 1;
this->day.version = 5;
return true;
} else if(day.version == 3) {
this->day = day;
for(uint8_t i = 0; i < 24; i++) this->day.hExport[i] = 0;
this->day.version = 4;
this->day.accuracy = 1;
this->day.version = 5;
return true;
}
return false;
}
bool AmsDataStorage::setMonthData(MonthDataPoints& month) {
if(month.version == 5) {
if(month.version == 6) {
this->month = month;
return true;
} else if(month.version == 5) {
this->month = month;
this->month.accuracy = 1;
this->month.version = 6;
return true;
} else if(month.version == 4) {
this->month = month;
for(uint8_t i = 0; i < 31; i++) this->month.dExport[i] = 0;
this->month.version = 5;
this->month.accuracy = 1;
this->month.version = 6;
return true;
}
return false;
}
uint8_t AmsDataStorage::getDayAccuracy() {
return day.accuracy;
}
void AmsDataStorage::setDayAccuracy(uint8_t accuracy) {
if(day.accuracy != accuracy) {
uint16_t multiplier = pow(10, day.accuracy)/pow(10, accuracy);
for(uint8_t i = 0; i < 24; i++) {
day.hImport[i] = day.hImport[i] * multiplier;
day.hExport[i] = day.hExport[i] * multiplier;
}
day.accuracy = accuracy;
}
}
uint8_t AmsDataStorage::getMonthAccuracy() {
return month.accuracy;
}
void AmsDataStorage::setMonthAccuracy(uint8_t accuracy) {
if(month.accuracy != accuracy) {
uint16_t multiplier = pow(10, month.accuracy)/pow(10, accuracy);
for(uint8_t i = 0; i < 31; i++) {
month.dImport[i] = month.dImport[i] * multiplier;
month.dExport[i] = month.dExport[i] * multiplier;
}
month.accuracy = accuracy;
}
month.accuracy = accuracy;
}
bool AmsDataStorage::isHappy() {
return isDayHappy() && isMonthHappy();
}

View File

@@ -40,17 +40,20 @@
"h" : {
"u" : %.2f,
"c" : %.2f,
"p" : %.2f
"p" : %.2f,
"i" : %.2f
},
"d" : {
"u" : %.2f,
"c" : %.2f,
"p" : %.2f
"p" : %.2f,
"i" : %.2f
},
"m" : {
"u" : %.2f,
"c" : %.2f,
"p" : %.2f
"p" : %.2f,
"i" : %.2f
}
},
"c" : %u

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -64,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

@@ -1,38 +1,31 @@
<form method="post" action="/save">
<input type="hidden" name="ec" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>ENTSO-E API</h6>
<h6>Price API</h6>
<div class="row">
<div class="col-xl-4 col-lg-6 col-md-8">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Token</span>
</div>
<input type="text" name="et" class="form-control" value="{et}"/>
</div>
</div>
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Region</span>
</div>
<select name="ea" class="form-control">
<option value="">None</option>
<optgroup label="Norway">
<option value="10YNO-1--------2" {eaNo1}>NO1</option>
<option value="10YNO-2--------T" {eaNo2}>NO2</option>
<option value="10YNO-3--------J" {eaNo3}>NO3</option>
<option value="10YNO-4--------9" {eaNo4}>NO4</option>
<option value="10Y1001A1001A48H" {eaNo5}>NO5</option>
<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" {eaSe1}>SE1</option>
<option value="10Y1001A1001A45N" {eaSe2}>SE2</option>
<option value="10Y1001A1001A46L" {eaSe3}>SE3</option>
<option value="10Y1001A1001A47J" {eaSe4}>SE4</option>
<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" {eaDk1}>DK1</option>
<option value="10YDK-2--------M" {eaDk2}>DK2</option>
<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>
@@ -56,10 +49,10 @@
<span class="input-group-text">Currency</span>
</div>
<select name="ecu" class="form-control">
<option value="NOK" {ecNOK}>NOK</option>
<option value="SEK" {ecSEK}>SEK</option>
<option value="DKK" {ecDKK}>DKK</option>
<option value="EUR" {ecEUR}>EUR</option>
<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>
@@ -71,6 +64,14 @@
<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/>

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

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

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>
@@ -89,7 +89,7 @@ private:
void configMqttHtml();
void configWebHtml();
void configDomoticzHtml();
void configEntsoeHtml();
void configPriceApiHtml();
void configNtpHtml();
void configGpioHtml();
void configDebugHtml();

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:

View File

@@ -18,7 +18,7 @@
#include "root/mqtt_html.h"
#include "root/web_html.h"
#include "root/domoticz_html.h"
#include "root/entsoe_html.h"
#include "root/priceapi_html.h"
#include "root/ntp_html.h"
#include "root/gpio_html.h"
#include "root/debugging_html.h"
@@ -71,7 +71,7 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, Meter
server.on(F("/mqtt"), HTTP_GET, std::bind(&AmsWebServer::configMqttHtml, this));
server.on(F("/web"), HTTP_GET, std::bind(&AmsWebServer::configWebHtml, this));
server.on(F("/domoticz"),HTTP_GET, std::bind(&AmsWebServer::configDomoticzHtml, this));
server.on(F("/entsoe"),HTTP_GET, std::bind(&AmsWebServer::configEntsoeHtml, this));
server.on(F("/priceapi"),HTTP_GET, std::bind(&AmsWebServer::configPriceApiHtml, this));
server.on(F("/thresholds"),HTTP_GET, std::bind(&AmsWebServer::configThresholdsHtml, this));
server.on(F("/boot.css"), HTTP_GET, std::bind(&AmsWebServer::bootCss, this));
server.on(F("/github.svg"), HTTP_GET, std::bind(&AmsWebServer::githubSvg, this));
@@ -544,8 +544,8 @@ void AmsWebServer::configDomoticzHtml() {
server.sendContent_P(FOOT_HTML);
}
void AmsWebServer::configEntsoeHtml() {
printD(F("Serving /entsoe.html over http..."));
void AmsWebServer::configPriceApiHtml() {
printD(F("Serving /priceapi.html over http..."));
if(!checkSecurity(1))
return;
@@ -553,52 +553,54 @@ void AmsWebServer::configEntsoeHtml() {
EntsoeConfig entsoe;
config->getEntsoeConfig(entsoe);
if(ESP.getFreeHeap() > 25000) {
String html = String((const __FlashStringHelper*) ENTSOE_HTML);
String html = String((const __FlashStringHelper*) PRICEAPI_HTML);
html.replace(F("{et}"), entsoe.token);
html.replace(F("{em}"), String(entsoe.multiplier / 1000.0, 3));
html.replace(F("{eaNo1}"), strcmp(entsoe.area, "10YNO-1--------2") == 0 ? F("selected") : F(""));
html.replace(F("{eaNo2}"), strcmp(entsoe.area, "10YNO-2--------T") == 0 ? F("selected") : F(""));
html.replace(F("{eaNo3}"), strcmp(entsoe.area, "10YNO-3--------J") == 0 ? F("selected") : F(""));
html.replace(F("{eaNo4}"), strcmp(entsoe.area, "10YNO-4--------9") == 0 ? F("selected") : F(""));
html.replace(F("{eaNo5}"), strcmp(entsoe.area, "10Y1001A1001A48H") == 0 ? F("selected") : F(""));
html.replace(F("{eaSe1}"), strcmp(entsoe.area, "10Y1001A1001A44P") == 0 ? F("selected") : F(""));
html.replace(F("{eaSe2}"), strcmp(entsoe.area, "10Y1001A1001A45N") == 0 ? F("selected") : F(""));
html.replace(F("{eaSe3}"), strcmp(entsoe.area, "10Y1001A1001A46L") == 0 ? F("selected") : F(""));
html.replace(F("{eaSe4}"), strcmp(entsoe.area, "10Y1001A1001A47J") == 0 ? F("selected") : F(""));
html.replace(F("{eaDk1}"), strcmp(entsoe.area, "10YDK-1--------W") == 0 ? F("selected") : F(""));
html.replace(F("{eaDk2}"), strcmp(entsoe.area, "10YDK-2--------M") == 0 ? F("selected") : F(""));
html.replace(F("{at}"), strcmp(entsoe.area, "10YAT-APG------L") == 0 ? F("selected") : F(""));
html.replace(F("{be}"), strcmp(entsoe.area, "10YBE----------2") == 0 ? F("selected") : F(""));
html.replace(F("{cz}"), strcmp(entsoe.area, "10YCZ-CEPS-----N") == 0 ? F("selected") : F(""));
html.replace(F("{ee}"), strcmp(entsoe.area, "10Y1001A1001A39I") == 0 ? F("selected") : F(""));
html.replace(F("{fi}"), strcmp(entsoe.area, "10YFI-1--------U") == 0 ? F("selected") : F(""));
html.replace(F("{fr}"), strcmp(entsoe.area, "10YFR-RTE------C") == 0 ? F("selected") : F(""));
html.replace(F("{de}"), strcmp(entsoe.area, "10Y1001A1001A83F") == 0 ? F("selected") : F(""));
html.replace(F("{gb}"), strcmp(entsoe.area, "10YGB----------A") == 0 ? F("selected") : F(""));
html.replace(F("{lv}"), strcmp(entsoe.area, "10YLV-1001A00074") == 0 ? F("selected") : F(""));
html.replace(F("{lt}"), strcmp(entsoe.area, "10YLT-1001A0008Q") == 0 ? F("selected") : F(""));
html.replace(F("{nl}"), strcmp(entsoe.area, "10YNL----------L") == 0 ? F("selected") : F(""));
html.replace(F("{pl}"), strcmp(entsoe.area, "10YPL-AREA-----S") == 0 ? F("selected") : F(""));
html.replace(F("{ch}"), strcmp(entsoe.area, "10YCH-SWISSGRIDZ") == 0 ? F("selected") : F(""));
html.replace(F("{ecNOK}"), strcmp(entsoe.currency, "NOK") == 0 ? F("selected") : F(""));
html.replace(F("{ecSEK}"), strcmp(entsoe.currency, "SEK") == 0 ? F("selected") : F(""));
html.replace(F("{ecDKK}"), strcmp(entsoe.currency, "DKK") == 0 ? F("selected") : F(""));
html.replace(F("{ecEUR}"), strcmp(entsoe.currency, "EUR") == 0 ? F("selected") : F(""));
server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN);
server.send_P(200, MIME_HTML, HEAD_HTML);
server.sendContent(html);
server.sendContent_P(FOOT_HTML);
if(ESP.getFreeHeap() > 32000) {
html.replace("{et}", entsoe.token);
html.replace("{dt}", "");
} else {
notFound();
html.replace("{et}", "");
html.replace("{dt}", "d-none");
}
html.replace("{em}", String(entsoe.multiplier / 1000.0, 3));
html.replace(F("{no1}"), strcmp(entsoe.area, "10YNO-1--------2") == 0 ? "selected" : "");
html.replace(F("{no2}"), strcmp(entsoe.area, "10YNO-2--------T") == 0 ? "selected" : "");
html.replace(F("{no3}"), strcmp(entsoe.area, "10YNO-3--------J") == 0 ? "selected" : "");
html.replace(F("{no4}"), strcmp(entsoe.area, "10YNO-4--------9") == 0 ? "selected" : "");
html.replace(F("{no5}"), strcmp(entsoe.area, "10Y1001A1001A48H") == 0 ? "selected" : "");
html.replace(F("{se1}"), strcmp(entsoe.area, "10Y1001A1001A44P") == 0 ? "selected" : "");
html.replace(F("{se2}"), strcmp(entsoe.area, "10Y1001A1001A45N") == 0 ? "selected" : "");
html.replace(F("{se3}"), strcmp(entsoe.area, "10Y1001A1001A46L") == 0 ? "selected" : "");
html.replace(F("{se4}"), strcmp(entsoe.area, "10Y1001A1001A47J") == 0 ? "selected" : "");
html.replace(F("{dk1}"), strcmp(entsoe.area, "10YDK-1--------W") == 0 ? "selected" : "");
html.replace(F("{dk2}"), strcmp(entsoe.area, "10YDK-2--------M") == 0 ? "selected" : "");
html.replace(F("{at}"), strcmp(entsoe.area, "10YAT-APG------L") == 0 ? F("selected") : F(""));
html.replace(F("{be}"), strcmp(entsoe.area, "10YBE----------2") == 0 ? F("selected") : F(""));
html.replace(F("{cz}"), strcmp(entsoe.area, "10YCZ-CEPS-----N") == 0 ? F("selected") : F(""));
html.replace(F("{ee}"), strcmp(entsoe.area, "10Y1001A1001A39I") == 0 ? F("selected") : F(""));
html.replace(F("{fi}"), strcmp(entsoe.area, "10YFI-1--------U") == 0 ? F("selected") : F(""));
html.replace(F("{fr}"), strcmp(entsoe.area, "10YFR-RTE------C") == 0 ? F("selected") : F(""));
html.replace(F("{de}"), strcmp(entsoe.area, "10Y1001A1001A83F") == 0 ? F("selected") : F(""));
html.replace(F("{gb}"), strcmp(entsoe.area, "10YGB----------A") == 0 ? F("selected") : F(""));
html.replace(F("{lv}"), strcmp(entsoe.area, "10YLV-1001A00074") == 0 ? F("selected") : F(""));
html.replace(F("{lt}"), strcmp(entsoe.area, "10YLT-1001A0008Q") == 0 ? F("selected") : F(""));
html.replace(F("{nl}"), strcmp(entsoe.area, "10YNL----------L") == 0 ? F("selected") : F(""));
html.replace(F("{pl}"), strcmp(entsoe.area, "10YPL-AREA-----S") == 0 ? F("selected") : F(""));
html.replace(F("{ch}"), strcmp(entsoe.area, "10YCH-SWISSGRIDZ") == 0 ? F("selected") : F(""));
html.replace(F("{nok}"), strcmp(entsoe.currency, "NOK") == 0 ? "selected" : "");
html.replace(F("{sek}"), strcmp(entsoe.currency, "SEK") == 0 ? "selected" : "");
html.replace(F("{dkk}"), strcmp(entsoe.currency, "DKK") == 0 ? "selected" : "");
html.replace(F("{eur}"), strcmp(entsoe.currency, "EUR") == 0 ? "selected" : "");
server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN);
server.send_P(200, MIME_HTML, HEAD_HTML);
server.sendContent(html);
server.sendContent_P(FOOT_HTML);
}
void AmsWebServer::configThresholdsHtml() {
@@ -725,7 +727,7 @@ void AmsWebServer::dataJson() {
}
float price = ENTSOE_NO_VALUE;
if(eapi != NULL && strlen(eapi->getToken()) > 0)
if(eapi != NULL)
price = eapi->getValueForHour(0);
String peaks = "";
@@ -733,7 +735,7 @@ void AmsWebServer::dataJson() {
if(peakCount > 5) peakCount = 5;
for(uint8_t i = 1; i <= peakCount; i++) {
if(!peaks.isEmpty()) peaks += ",";
peaks += String(ea->getPeak(i));
peaks += String(ea->getPeak(i).value / 100.0);
}
snprintf_P(buf, BufferSize, DATA_JSON,
@@ -777,12 +779,15 @@ void AmsWebServer::dataJson() {
ea->getUseThisHour(),
ea->getCostThisHour(),
ea->getProducedThisHour(),
ea->getIncomeThisHour(),
ea->getUseToday(),
ea->getCostToday(),
ea->getProducedToday(),
ea->getIncomeToday(),
ea->getUseThisMonth(),
ea->getCostThisMonth(),
ea->getProducedThisMonth(),
ea->getIncomeThisMonth(),
(uint32_t) time(nullptr)
);

View File

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

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;

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;
@@ -55,9 +67,15 @@ public:
double getCostThisMonth();
uint16_t getCostLastMonth();
double getIncomeThisHour();
double getIncomeToday();
double getIncomeYesterday();
double getIncomeThisMonth();
uint16_t getIncomeLastMonth();
float getMonthMax();
uint8_t getCurrentThreshold();
float getPeak(uint8_t);
EnergyAccountingPeak getPeak(uint8_t);
EnergyAccountingData getData();
void setData(EnergyAccountingData&);
@@ -72,7 +90,7 @@ private:
Timezone *tz = NULL;
uint8_t currentHour = 0, currentDay = 0, currentThresholdIdx = 0;
double use, costHour, costDay;
double produce;
double produce, incomeHour, incomeDay;
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0 };
void calcDayCost();

View File

@@ -45,8 +45,9 @@ bool EnergyAccounting::update(AmsData* amsData) {
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
@@ -58,6 +59,7 @@ bool EnergyAccounting::update(AmsData* amsData) {
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: %.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;
}
@@ -85,13 +87,18 @@ bool EnergyAccounting::update(AmsData* amsData) {
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;
}
@@ -100,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 };
}
@@ -127,6 +136,13 @@ bool EnergyAccounting::update(AmsData* amsData) {
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) {
@@ -144,13 +160,19 @@ void EnergyAccounting::calcDayCost() {
breakTime(tz->toLocal(now), local);
if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
if(initPrice) costDay = 0;
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;
}
@@ -230,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;
@@ -265,8 +307,8 @@ float EnergyAccounting::getMonthMax() {
return maxHour > 0 ? maxHour / count / 100.0 : 0.0;
}
float EnergyAccounting::getPeak(uint8_t num) {
if(num < 1 || num > 5) return 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 };
@@ -292,10 +334,10 @@ float EnergyAccounting::getPeak(uint8_t num) {
if(!included[i]) continue;
pos++;
if(pos == num) {
return data.peaks[i].value / 100.0;
return data.peaks[i];
}
}
return 0.0;
return EnergyAccountingPeak({0,0});
}
bool EnergyAccounting::load() {
@@ -313,14 +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,
@@ -329,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

View File

@@ -2,6 +2,7 @@
#define _ENTSOEA44PARSER_H
#include "Stream.h"
#include "PricesContainer.h"
#define DOCPOS_SEEK 0
#define DOCPOS_CURRENCY 1
@@ -26,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,33 +25,44 @@ 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)
uint64_t midnightMillis = 0;
uint8_t tomorrowFetchMinute = 15; // How many minutes over 13:00 should it fetch prices
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);
float getCurrencyMultiplier(const char* from, const char* to, time_t t);
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

@@ -106,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

@@ -0,0 +1,410 @@
#include "EntsoeApi.h"
#include <EEPROM.h>
#include "Uptime.h"
#include "TimeLib.h"
#include "DnbCurrParser.h"
#include "version.h"
#include "GcmParser.h"
#if defined(ESP32)
#include <esp_task_wdt.h>
#endif
EntsoeApi::EntsoeApi(RemoteDebug* Debug) {
this->buf = (char*) malloc(BufferSize);
debugger = Debug;
// Entso-E uses CET/CEST
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
tz = new Timezone(CEST, CET);
tomorrowFetchMinute = 15 + random(45); // Random between 13:15 and 14:00
}
void EntsoeApi::setup(EntsoeConfig& config) {
if(this->config == NULL) {
this->config = new EntsoeConfig();
}
memcpy(this->config, &config, sizeof(config));
lastTodayFetch = lastTomorrowFetch = lastCurrencyFetch = 0;
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() {
return this->config->token;
}
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);
}
float EntsoeApi::getValueForHour(time_t cur, int8_t hour) {
tmElements_t tm;
if(tz != NULL)
cur = tz->toLocal(cur);
breakTime(cur, tm);
int pos = tm.Hour + hour;
if(pos >= 48)
return ENTSOE_NO_VALUE;
double value = ENTSOE_NO_VALUE;
double multiplier = config->multiplier / 1000.0;
if(pos > 23) {
if(tomorrow == NULL)
return ENTSOE_NO_VALUE;
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->currency, config->currency, cur);
if(mult == 0) return ENTSOE_NO_VALUE;
multiplier *= mult;
} else if(pos >= 0) {
if(today == NULL)
return ENTSOE_NO_VALUE;
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->currency, config->currency, cur);
if(mult == 0) return ENTSOE_NO_VALUE;
multiplier *= mult;
}
return value * multiplier;
}
bool EntsoeApi::loop() {
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;
tmElements_t tm;
breakTime(tz->toLocal(t), tm);
if(currentDay == 0) {
currentDay = tm.Day;
currentHour = tm.Hour;
}
if(currentDay != tm.Day) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Rotating price objects at %lu\n", t);
if(today != NULL) delete today;
if(tomorrow != NULL) {
today = tomorrow;
tomorrow = NULL;
}
currentDay = tm.Day;
currentHour = tm.Hour;
return today != NULL; // Only trigger MQTT publish if we have todays prices.
} else if(currentHour != tm.Hour) {
currentHour = tm.Hour;
return today != NULL; // Only trigger MQTT publish if we have todays prices.
}
if(today == NULL && (lastTodayFetch == 0 || now - lastTodayFetch > 60000)) {
try {
lastTodayFetch = now;
today = fetchPrices(t);
} catch(const std::exception& e) {
if(lastError == 0) lastError = 900;
today = NULL;
}
return today != NULL; // Only trigger MQTT publish if we have todays prices.
}
// Prices for next day are published at 13:00 CE(S)T, but to avoid heavy server traffic at that time, we will
// fetch with one hour (with some random delay) and retry every 15 minutes
if(tomorrow == NULL && (tm.Hour > 13 || (tm.Hour == 13 && tm.Minute >= tomorrowFetchMinute)) && (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 900000)) {
try {
lastTomorrowFetch = now;
tomorrow = fetchPrices(t+SECS_PER_DAY);
} catch(const std::exception& e) {
if(lastError == 0) lastError = 900;
tomorrow = NULL;
}
return tomorrow != NULL;
}
return false;
}
bool EntsoeApi::retrieve(const char* url, Stream* doc) {
#if defined(ESP32)
if(http.begin(url)) {
printD("Connection established");
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#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");
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(http.errorToString(status));
printD(http.getString());
http.end();
return false;
}
} else {
return false;
}
#endif
return false;
}
float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to, time_t t) {
if(strcmp(from, to) == 0)
return 1.00;
uint64_t now = millis64();
if(now > lastCurrencyFetch && (lastCurrencyFetch == 0 || (now - lastCurrencyFetch) > 60000)) {
lastCurrencyFetch = now;
DnbCurrParser p;
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
snprintf(buf, BufferSize, "https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1", from);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Retrieving %s to NOK conversion\n", from);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf);
if(retrieve(buf, &p)) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue());
currencyMultiplier = p.getValue();
if(strncmp(to, "NOK", 3) != 0) {
snprintf(buf, BufferSize, "https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1", to);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Retrieving %s to NOK conversion\n", to);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf);
if(retrieve(buf, &p)) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue());
currencyMultiplier /= p.getValue();
} else {
return 0;
}
}
} else {
return 0;
}
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) Resulting currency multiplier: %.4f\n", currencyMultiplier);
tmElements_t tm;
breakTime(t, tm);
lastCurrencyFetch = now + (SECS_PER_DAY * 1000) - (((((tm.Hour * 60) + tm.Minute) * 60) + tm.Second) * 1000);
}
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);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args);
va_end(args);
}
void EntsoeApi::printE(String fmt, ...) {
va_list args;
va_start(args, fmt);
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args);
va_end(args);
}
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

@@ -22,12 +22,12 @@ HomeAssistantSensor HA_SENSORS[HA_SENSOR_COUNT] PROGMEM = {
{"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\""},
{"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\""},
{"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\""},
@@ -36,12 +36,12 @@ HomeAssistantSensor HA_SENSORS[HA_SENSOR_COUNT] PROGMEM = {
{"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\""},
{"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", ""},

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,14 +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/ha4_json.h"
#include "web/root/jsonsys_json.h"
#include "web/root/jsonprices_json.h"
#include "web/root/hadiscover_json.h"
#include "web/root/realtime_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())
@@ -86,7 +86,7 @@ bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, En
if(peakCount > 5) peakCount = 5;
for(uint8_t i = 1; i <= peakCount; i++) {
if(!peaks.isEmpty()) peaks += ",";
peaks += String(ea->getPeak(i), 2);
peaks += String(ea->getPeak(i).value / 100.0, 2);
}
snprintf_P(json, BufferSize, REALTIME_JSON,
ea->getMonthMax(),
@@ -136,7 +136,7 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(strlen(eapi->getToken()) == 0)
if(eapi->getValueForHour(0) == ENTSOE_NO_VALUE)
return false;
time_t now = time(nullptr);
@@ -280,12 +280,19 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, Energ
String uom = String(sensor.uom);
if(strncmp(sensor.devcl, "monetary", 8) == 0) {
if(eapi == NULL) continue;
uom = String(eapi->getCurrency());
if(strncmp(sensor.path, "prices", 5) == 0) {
uom = String(eapi->getCurrency()) + "/kWh";
} else {
uom = String(eapi->getCurrency());
}
}
if(strncmp(sensor.path, "peaks[", 6) == 0) {
if(peaks >= peakCount) continue;
peaks++;
}
if(strncmp(sensor.path, "temp", 4) == 0) {
if(hw->getTemperature() < 0) continue;
}
snprintf_P(json, BufferSize, HADISCOVER_JSON,
sensor.name,
topic.c_str(), sensor.topic,

View File

@@ -371,9 +371,9 @@ bool HwTools::ledOff(uint8_t color) {
bool HwTools::ledBlink(uint8_t color, uint8_t blink) {
for(int i = 0; i < blink; i++) {
if(!ledOn(color)) return false;
delay(50);
delay(75);
ledOff(color);
delay(200);
if(i != blink) delay(75);
}
return true;
}

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