From f2dda26bbbca146c35ca92a7b6734c3f00d0d5cc Mon Sep 17 00:00:00 2001 From: Gunnar Skjold Date: Sun, 10 Jan 2021 20:54:25 +0100 Subject: [PATCH] Added support for retrieving energy price from ENTSO-E API --- src/AmsConfiguration.cpp | 191 ++++++++++++------------ src/AmsConfiguration.h | 92 +++++++++++- src/AmsToMqttBridge.ino | 147 ++++++++++++++++++- src/entsoe/DnbCurrParser.cpp | 60 ++++++++ src/entsoe/DnbCurrParser.h | 25 ++++ src/entsoe/EntsoeA44Parser.cpp | 102 +++++++++++++ src/entsoe/EntsoeA44Parser.h | 38 +++++ src/entsoe/EntsoeApi.cpp | 261 +++++++++++++++++++++++++++++++++ src/entsoe/EntsoeApi.h | 50 +++++++ src/web/AmsWebServer.cpp | 147 ++++++++++++++++++- src/web/AmsWebServer.h | 7 +- web/application.js | 11 +- web/configdomoticz.html | 12 -- web/configmqtt.html | 1 + web/configweb.html | 1 + web/configwifi.html | 1 + web/entsoe.html | 72 +++++++++ web/head.html | 15 +- web/price.html | 148 +++++++++++++++++++ 19 files changed, 1252 insertions(+), 129 deletions(-) create mode 100644 src/entsoe/DnbCurrParser.cpp create mode 100644 src/entsoe/DnbCurrParser.h create mode 100644 src/entsoe/EntsoeA44Parser.cpp create mode 100644 src/entsoe/EntsoeA44Parser.h create mode 100644 src/entsoe/EntsoeApi.cpp create mode 100644 src/entsoe/EntsoeApi.h create mode 100644 web/entsoe.html create mode 100644 web/price.html diff --git a/src/AmsConfiguration.cpp b/src/AmsConfiguration.cpp index 5e4dec92..05ed34c0 100644 --- a/src/AmsConfiguration.cpp +++ b/src/AmsConfiguration.cpp @@ -634,6 +634,40 @@ void AmsConfiguration::clearNtp() { } +char* AmsConfiguration::getEntsoeApiToken() { + return config.entsoeApiToken; +} + +void AmsConfiguration::setEntsoeApiToken(const char* token) { + strcpy(config.entsoeApiToken, token); +} + +char* AmsConfiguration::getEntsoeApiArea() { + return config.entsoeApiArea; +} + +void AmsConfiguration::setEntsoeApiArea(const char* area) { + strcpy(config.entsoeApiArea, area); +} + +char* AmsConfiguration::getEntsoeApiCurrency() { + return config.entsoeApiCurrency; +} + +void AmsConfiguration::setEntsoeApiCurrency(const char* currency) { + strcpy(config.entsoeApiCurrency, currency); +} + +double AmsConfiguration::getEntsoeApiMultiplier() { + return config.entsoeApiMultiplier; +} + +void AmsConfiguration::setEntsoeApiMultiplier(double multiplier) { + config.entsoeApiMultiplier = multiplier; +} + + + void AmsConfiguration::clear() { clearMeter(); clearWifi(); @@ -662,6 +696,7 @@ bool AmsConfiguration::hasConfig() { case 81: case 82: case 83: + case 84: return true; default: configVersion = 0; @@ -680,13 +715,13 @@ bool AmsConfiguration::load() { EEPROM.begin(EEPROM_SIZE); int cs = EEPROM.read(address++); switch(cs) { - case 81: // v1.2 - success = loadConfig81(address); - break; case 82: // v1.3 success = loadConfig82(address); break; case 83: // v1.4 + success = loadConfig83(address); + break; + case 84: // v1.5 EEPROM.get(address, config); loadTempSensors(); success = true; @@ -787,99 +822,73 @@ bool AmsConfiguration::loadConfig82(int address) { config.domoCL1IDX = config82.domoCL1IDX; } -bool AmsConfiguration::loadConfig81(int address) { - char* temp; +bool AmsConfiguration::loadConfig83(int address) { + ConfigObject83 config83; + EEPROM.get(address, config83); + config.boardType = config83.boardType; + strcpy(config.wifiSsid, config83.wifiSsid); + strcpy(config.wifiPassword, config83.wifiPassword); + strcpy(config.wifiIp, config83.wifiIp); + strcpy(config.wifiGw, config83.wifiGw); + strcpy(config.wifiSubnet, config83.wifiSubnet); + strcpy(config.wifiDns1, config83.wifiDns1); + strcpy(config.wifiDns2, config83.wifiDns2); + strcpy(config.wifiHostname, config83.wifiHostname); + strcpy(config.mqttHost, config83.mqttHost); + config.mqttPort = config83.mqttPort; + strcpy(config.mqttClientId, config83.mqttClientId); + strcpy(config.mqttPublishTopic, config83.mqttPublishTopic); + strcpy(config.mqttSubscribeTopic, config83.mqttSubscribeTopic); + strcpy(config.mqttUser, config83.mqttUser); + strcpy(config.mqttPassword, config83.mqttPassword); + config.mqttPayloadFormat = config83.mqttPayloadFormat; + config.mqttSsl = config83.mqttSsl; + config.authSecurity = config83.authSecurity; + strcpy(config.authUser, config83.authUser); + strcpy(config.authPassword, config83.authPassword); + + config.meterType = config83.meterType; + config.distributionSystem = config83.distributionSystem; + config.mainFuse = config83.mainFuse; + config.productionCapacity = config83.productionCapacity; + memcpy(config.meterEncryptionKey, config83.meterEncryptionKey, 16); + memcpy(config.meterAuthenticationKey, config83.meterAuthenticationKey, 16); + config.substituteMissing = config83.substituteMissing; + config.sendUnknown = config83.sendUnknown; - address += readString(address, &temp); - setWifiSsid(temp); - address += readString(address, &temp); - setWifiPassword(temp); + config.debugTelnet = config83.debugTelnet; + config.debugSerial = config83.debugSerial; + config.debugLevel = config83.debugLevel; - bool staticIp = false; - address += readBool(address, &staticIp); - if(staticIp) { - address += readString(address, &temp); - setWifiIp(temp); - address += readString(address, &temp); - setWifiGw(temp); - address += readString(address, &temp); - setWifiSubnet(temp); - address += readString(address, &temp); - setWifiDns1(temp); - address += readString(address, &temp); - setWifiDns2(temp); - } - address += readString(address, &temp); - setWifiHostname(temp); - bool mqtt = false; - address += readBool(address, &mqtt); - if(mqtt) { - address += readString(address, &temp); - setMqttHost(temp); - int port; - address += readInt(address, &port); - setMqttPort(port); - address += readString(address, &temp); - setMqttClientId(temp); - address += readString(address, &temp); - setMqttPublishTopic(temp); - address += readString(address, &temp); - setMqttSubscribeTopic(temp); + config.hanPin = config83.hanPin; + config.apPin = config83.apPin; + config.ledPin = config83.ledPin; + config.ledInverted = config83.ledInverted; + config.ledPinRed = config83.ledPinRed; + config.ledPinGreen = config83.ledPinGreen; + config.ledPinBlue = config83.ledPinBlue; + config.ledRgbInverted = config83.ledRgbInverted; + config.tempSensorPin = config83.tempSensorPin; + config.vccPin = config83.vccPin; + config.vccOffset = config83.vccOffset; + config.vccMultiplier = config83.vccMultiplier; + config.vccBootLimit = config83.vccBootLimit; - bool secure = false; - address += readBool(address, &secure); - if (secure) - { - address += readString(address, &temp); - setMqttUser(temp); - address += readString(address, &temp); - setMqttPassword(temp); - } else { - setMqttUser(""); - setMqttPassword(""); - } - int payloadFormat; - address += readInt(address, &payloadFormat); - setMqttPayloadFormat(payloadFormat); - } else { - clearMqtt(); - } + config.domoELIDX = config83.domoELIDX; + config.domoVL1IDX = config83.domoVL1IDX; + config.domoVL2IDX = config83.domoVL2IDX; + config.domoVL3IDX = config83.domoVL3IDX; + config.domoCL1IDX = config83.domoCL1IDX; - byte b; - address += readByte(address, &b); - setAuthSecurity(b); - if (b > 0) { - address += readString(address, &temp); - setAuthUser(temp); - address += readString(address, &temp); - setAuthPassword(temp); - } else { - clearAuth(); - } + config.mDnsEnable = config83.mDnsEnable; + config.ntpEnable = config83.ntpEnable; + config.ntpDhcp = config83.ntpDhcp; + config.ntpOffset = config83.ntpOffset; + config.ntpSummerOffset = config83.ntpSummerOffset; + strcpy(config.ntpServer, config83.ntpServer); - int i; - address += readInt(address, &i); - setMeterType(i); - address += readInt(address, &i); - setDistributionSystem(i); - address += readInt(address, &i); - setMainFuse(i); - address += readInt(address, &i); - setProductionCapacity(i); - - bool debugTelnet = false; - address += readBool(address, &debugTelnet); - setDebugTelnet(debugTelnet); - bool debugSerial = false; - address += readBool(address, &debugSerial); - setDebugSerial(debugSerial); - address += readInt(address, &i); - setDebugLevel(i); - - ackWifiChange(); - - return true; -} + config.tempAnalogSensorPin = config83.tempAnalogSensorPin; +} bool AmsConfiguration::save() { int address = EEPROM_CONFIG_ADDRESS; diff --git a/src/AmsConfiguration.h b/src/AmsConfiguration.h index c0ce1783..71856e18 100644 --- a/src/AmsConfiguration.h +++ b/src/AmsConfiguration.h @@ -39,6 +39,76 @@ struct ConfigObject { bool debugSerial; uint8_t debugLevel; + uint8_t hanPin; + uint8_t apPin; + uint8_t ledPin; + bool ledInverted; + uint8_t ledPinRed; + uint8_t ledPinGreen; + uint8_t ledPinBlue; + bool ledRgbInverted; + uint8_t tempSensorPin; + uint8_t tempAnalogSensorPin; + uint8_t vccPin; + int16_t vccOffset; + uint16_t vccMultiplier; + uint8_t vccBootLimit; + + uint16_t domoELIDX; + uint16_t domoVL1IDX; + uint16_t domoVL2IDX; + uint16_t domoVL3IDX; + uint16_t domoCL1IDX; + + bool mDnsEnable; + bool ntpEnable; + bool ntpDhcp; + int16_t ntpOffset; + int16_t ntpSummerOffset; + char ntpServer[64]; + + char entsoeApiToken[37]; + char entsoeApiArea[17]; + char entsoeApiCurrency[4]; + double entsoeApiMultiplier; +}; + +struct ConfigObject83 { + uint8_t boardType; + char wifiSsid[32]; + char wifiPassword[64]; + char wifiIp[15]; + char wifiGw[15]; + char wifiSubnet[15]; + char wifiDns1[15]; + char wifiDns2[15]; + char wifiHostname[32]; + char mqttHost[128]; + uint16_t mqttPort; + char mqttClientId[32]; + char mqttPublishTopic[64]; + char mqttSubscribeTopic[64]; + char mqttUser[64]; + char mqttPassword[64]; + uint8_t mqttPayloadFormat; + bool mqttSsl; + uint8_t authSecurity; + char authUser[64]; + char authPassword[64]; + + uint8_t meterType; + uint8_t distributionSystem; + uint8_t mainFuse; + uint8_t productionCapacity; + uint8_t meterEncryptionKey[16]; + uint8_t meterAuthenticationKey[16]; + bool substituteMissing; + bool sendUnknown; + + bool debugTelnet; + bool debugSerial; + uint8_t debugLevel; + uint8_t hanPin; uint8_t apPin; uint8_t ledPin; @@ -91,6 +161,7 @@ struct ConfigObject82 { uint8_t authSecurity; char authUser[64]; char authPassword[64]; + uint8_t meterType; uint8_t distributionSystem; uint8_t mainFuse; @@ -289,6 +360,15 @@ public: bool isNtpChanged(); void ackNtpChange(); + char* getEntsoeApiToken(); + void setEntsoeApiToken(const char* token); + char* getEntsoeApiArea(); + void setEntsoeApiArea(const char* area); + char* getEntsoeApiCurrency(); + void setEntsoeApiCurrency(const char* currency); + double getEntsoeApiMultiplier(); + void setEntsoeApiMultiplier(double multiplier); + uint8_t getTempSensorCount(); TempSensorConfig* getTempSensorConfig(uint8_t i); void updateTempSensorConfig(uint8_t address[8], const char name[32], bool common); @@ -343,6 +423,7 @@ private: 0xFF, // Blue true, // Inverted 0xFF, // Temp sensor + 0xFF, // Analog temp sensor 0xFF, // Vcc 0, // Offset 100, // Multiplier @@ -359,8 +440,11 @@ private: 360, // Timezone (*10) 360, // Summertime offset (*10) "pool.ntp.org", // NTP server - 0xFF, // Analog temp sensor - // 894 bytes + "", // Entsoe token + "", // Entsoe area + "", // Entsoe currency + 1.00, // Entsoe multiplier + // 960 bytes }; bool wifiChanged, mqttChanged, meterChanged = true, domoChanged, ntpChanged; @@ -368,15 +452,15 @@ private: TempSensorConfig* tempSensors[32]; const int EEPROM_SIZE = 1024 * 3; - const int EEPROM_CHECK_SUM = 83; // Used to check if config is stored. Change if structure changes + const int EEPROM_CHECK_SUM = 84; // Used to check if config is stored. Change if structure changes const int EEPROM_CONFIG_ADDRESS = 0; const int EEPROM_TEMP_CONFIG_ADDRESS = 2048; void loadTempSensors(); void saveTempSensors(); - bool loadConfig81(int address); bool loadConfig82(int address); + bool loadConfig83(int address); int readString(int pAddress, char* pString[]); int readInt(int pAddress, int *pValue); diff --git a/src/AmsToMqttBridge.ino b/src/AmsToMqttBridge.ino index 8ad16572..c8108e43 100644 --- a/src/AmsToMqttBridge.ino +++ b/src/AmsToMqttBridge.ino @@ -29,6 +29,7 @@ ADC_MODE(ADC_VCC); #endif #include "HwTools.h" +#include "entsoe/EntsoeApi.h" #include "web/AmsWebServer.h" #include "AmsConfiguration.h" @@ -56,7 +57,11 @@ AmsConfiguration config; RemoteDebug Debug; -AmsWebServer ws(&Debug, &hw); +EntsoeApi eapi(&Debug); + +Timezone* tz; + +AmsWebServer ws(&Debug, &hw, &eapi); MQTTClient mqtt(512); @@ -67,6 +72,10 @@ Stream *hanSerial; void setup() { if(config.hasConfig()) { config.load(); + + TimeChangeRule std = {"STD", Last, Sun, Oct, 3, config.getNtpOffset() / 60}; + TimeChangeRule dst = {"DST", Last, Sun, Mar, 2, (config.getNtpOffset() + config.getNtpSummerOffset()) / 60}; + tz = new Timezone(dst, std); } if(!config.hasConfig() || config.getConfigVersion() < 81) { @@ -136,6 +145,10 @@ void setup() { hw.ledBlink(LED_YELLOW, 1); hw.ledBlink(LED_GREEN, 1); hw.ledBlink(LED_BLUE, 1); + eapi.setToken(config.getEntsoeApiToken()); + eapi.setArea(config.getEntsoeApiArea()); + eapi.setCurrency(config.getEntsoeApiCurrency()); + eapi.setMultiplier(config.getEntsoeApiMultiplier()); if(config.getHanPin() == 3) { switch(config.getMeterType()) { @@ -445,7 +458,12 @@ void loop() { } delay(1); readHanPort(); - ws.loop(); + if(WiFi.status() == WL_CONNECTED) { + ws.loop(); + if(eapi.loop()) { + sendPricesToMqtt(); + } + } delay(1); // Needed for auto modem sleep } @@ -592,6 +610,129 @@ void mqttMessageReceived(String &topic, String &payload) // Ideas could be to query for values or to initiate OTA firmware update } +void sendPricesToMqtt() { + double min1hr, min3hr, min6hr; + int min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1; + double min = INT16_MAX, max = INT16_MIN; + double values[48]; + for(int i = 0; i < 48; i++) { + double val1 = eapi.getValueForHour(i); + values[i] = val1; + + if(val1 == ENTSOE_NO_VALUE) break; + + if(val1 < min) min = val1; + if(val1 > max) max = val1; + + if(i >= 24) continue; // Only estimate 1hr, 3hr and 6hr cheapest interval for next 24 hrs + + if(min1hrIdx == -1 || min1hr > val1) { + min1hr = val1; + min1hrIdx = i; + } + + double val2 = eapi.getValueForHour(i+1); + double val3 = eapi.getValueForHour(i+2); + if(val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE) continue; + double val3hr = val1+val2+val3; + if(min3hrIdx == -1 || min3hr > val3hr) { + min3hr = val3hr; + min3hrIdx = i; + } + + double val4 = eapi.getValueForHour(i+3); + double val5 = eapi.getValueForHour(i+4); + double val6 = eapi.getValueForHour(i+5); + if(val4 == ENTSOE_NO_VALUE || val5 == ENTSOE_NO_VALUE || val6 == ENTSOE_NO_VALUE) continue; + double val6hr = val1+val2+val3+val4+val5+val6; + if(min6hrIdx == -1 || min6hr > val6hr) { + min6hr = val6hr; + min6hrIdx = i; + } + } + + char ts1hr[21]; + if(min1hrIdx != -1) { + tmElements_t tm; + breakTime(time(nullptr) + (SECS_PER_HOUR * min1hrIdx), tm); + sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour); + } + char ts3hr[21]; + if(min3hrIdx != -1) { + tmElements_t tm; + breakTime(time(nullptr) + (SECS_PER_HOUR * min3hrIdx), tm); + sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour); + } + char ts6hr[21]; + if(min6hrIdx != -1) { + tmElements_t tm; + breakTime(time(nullptr) + (SECS_PER_HOUR * min6hrIdx), tm); + sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour); + } + + switch(config.getMqttPayloadFormat()) { + case 0: // JSON + { + StaticJsonDocument<512> json; + json["id"] = WiFi.macAddress(); + json["name"] = config.getMqttClientId(); + json["up"] = millis(); + JsonObject jp = json.createNestedObject("prices"); + for(int i = 0; i < 48; i++) { + double val = values[i]; + if(val == ENTSOE_NO_VALUE) break; + jp[String(i)] = serialized(String(val, 4)); + } + if(min != INT16_MAX) { + jp["min"] = serialized(String(min, 4)); + } + if(max != INT16_MIN) { + jp["max"] = serialized(String(max, 4)); + } + if(min1hrIdx != -1) { + jp["cheapest1hr"] = String(ts1hr); + } + if(min3hrIdx != -1) { + jp["cheapest3hr"] = String(ts1hr); + } + if(min6hrIdx != -1) { + jp["cheapest6hr"] = String(ts1hr); + } + break; + } + case 1: // RAW + case 2: + // Send updated prices if we have them + if(strcmp(config.getEntsoeApiToken(), "") != 0) { + for(int i = 0; i < 48; i++) { + double val = values[i]; + if(val == ENTSOE_NO_VALUE) { + mqtt.publish(String(config.getMqttPublishTopic()) + "/price/" + String(i), ""); + break; + } else { + mqtt.publish(String(config.getMqttPublishTopic()) + "/price/" + String(i), String(val, 4)); + } + } + if(min != INT16_MAX) { + mqtt.publish(String(config.getMqttPublishTopic()) + "/price/min", String(min, 4)); + } + if(max != INT16_MIN) { + mqtt.publish(String(config.getMqttPublishTopic()) + "/price/max", String(max, 4)); + } + if(min1hrIdx != -1) { + mqtt.publish(String(config.getMqttPublishTopic()) + "/price/cheapest/1hr", String(ts1hr)); + } + if(min3hrIdx != -1) { + mqtt.publish(String(config.getMqttPublishTopic()) + "/price/cheapest/3hr", String(ts3hr)); + } + if(min6hrIdx != -1) { + mqtt.publish(String(config.getMqttPublishTopic()) + "/price/cheapest/6hr", String(ts6hr)); + } + } + break; + } +} + int currentMeterType = 0; AmsData lastMqttData; void readHanPort() { @@ -769,6 +910,8 @@ void readHanPort() { mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/import/active/accumulated", String(data.getActiveImportCounter(), 2)); mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/export/reactive/accumulated", String(data.getReactiveExportCounter(), 2)); mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/export/active/accumulated", String(data.getActiveExportCounter(), 2)); + + sendPricesToMqtt(); case 2: // Only send data if changed. ID and Type is sent on the 10s interval only if changed if(lastMqttData.getMeterId() != data.getMeterId() || config.getMqttPayloadFormat() == 2) { diff --git a/src/entsoe/DnbCurrParser.cpp b/src/entsoe/DnbCurrParser.cpp new file mode 100644 index 00000000..52e6c738 --- /dev/null +++ b/src/entsoe/DnbCurrParser.cpp @@ -0,0 +1,60 @@ +#include "DnbCurrParser.h" +#include "HardwareSerial.h" + +double DnbCurrParser::getValue() { + return value; +} + +int DnbCurrParser::available() { + +} + +int DnbCurrParser::read() { + +} + +int DnbCurrParser::peek() { + +} + +void DnbCurrParser::flush() { + +} + +size_t DnbCurrParser::write(const uint8_t *buffer, size_t size) { + for(int i = 0; i < size; i++) { + write(buffer[i]); + } + return size; +} + +size_t DnbCurrParser::write(uint8_t byte) { + if(pos == 0) { + if(byte == '<') { + buf[pos++] = byte; + } + } else if(byte == '>') { + buf[pos++] = byte; + if(strncmp(buf, "') { + buf[pos++] = byte; + buf[pos] = '\0'; + if(strcmp(buf, "") == 0) { + docPos = DOCPOS_CURRENCY; + } else if(strcmp(buf, "") == 0) { + docPos = DOCPOS_MEASUREMENTUNIT; + } else if(strcmp(buf, "") == 0) { + docPos = DOCPOS_POSITION; + pointNum = 0xFF; + } else if(strcmp(buf, "") == 0) { + docPos = DOCPOS_AMOUNT; + } + pos = 0; + } else { + buf[pos++] = byte; + } + } + return 1; +} diff --git a/src/entsoe/EntsoeA44Parser.h b/src/entsoe/EntsoeA44Parser.h new file mode 100644 index 00000000..30804f0b --- /dev/null +++ b/src/entsoe/EntsoeA44Parser.h @@ -0,0 +1,38 @@ +#ifndef _ENTSOEA44PARSER_H +#define _ENTSOEA44PARSER_H + +#include "Stream.h" + +#define DOCPOS_SEEK 0 +#define DOCPOS_CURRENCY 1 +#define DOCPOS_MEASUREMENTUNIT 2 +#define DOCPOS_POSITION 3 +#define DOCPOS_AMOUNT 4 + +class EntsoeA44Parser: public Stream { +public: + EntsoeA44Parser(); + + char* getCurrency(); + char* getMeasurementUnit(); + double getPoint(uint8_t position); + + int available(); + int read(); + int peek(); + void flush(); + size_t write(const uint8_t *buffer, size_t size); + size_t write(uint8_t); + +private: + char currency[4]; + char measurementUnit[4]; + double points[24]; + + char buf[256]; + uint8_t pos = 0; + uint8_t docPos = 0; + uint8_t pointNum = 0; +}; + +#endif diff --git a/src/entsoe/EntsoeApi.cpp b/src/entsoe/EntsoeApi.cpp new file mode 100644 index 00000000..3ec8dcdb --- /dev/null +++ b/src/entsoe/EntsoeApi.cpp @@ -0,0 +1,261 @@ +#include "EntsoeApi.h" +#include +#include "Uptime.h" +#include "Time.h" +#include "DnbCurrParser.h" + +#if defined(ESP8266) + #include +#elif defined(ESP32) // ARDUINO_ARCH_ESP32 + #include +#else + #warning "Unsupported board type" +#endif + +EntsoeApi::EntsoeApi(RemoteDebug* Debug) { + debugger = Debug; + + // Entso-E uses CET/CEST + TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; + TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; + tz = new Timezone(CEST, CET); +} + +void EntsoeApi::setToken(const char* token) { + strcpy(this->token, token); +} + +void EntsoeApi::setArea(const char* area) { + strcpy(this->area, area); +} + +void EntsoeApi::setCurrency(const char* currency) { + strcpy(this->currency, currency); +} + +void EntsoeApi::setMultiplier(double multiplier) { + this->multiplier = multiplier; +} + +char* EntsoeApi::getCurrency() { + return currency; +} + +double EntsoeApi::getValueForHour(int hour) { + tmElements_t tm; + time_t cur = time(nullptr); + 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 = this->multiplier; + if(pos > 23) { + if(tomorrow == NULL) + return ENTSOE_NO_VALUE; + value = tomorrow->getPoint(pos-24); + if(strcmp(tomorrow->getMeasurementUnit(), "MWH") == 0) { + multiplier *= 0.001; + } else { + return ENTSOE_NO_VALUE; + } + multiplier *= getCurrencyMultiplier(tomorrow->getCurrency(), currency); + } else { + if(today == NULL) + return ENTSOE_NO_VALUE; + value = today->getPoint(pos); + if(strcmp(today->getMeasurementUnit(), "MWH") == 0) { + multiplier *= 0.001; + } else { + return ENTSOE_NO_VALUE; + } + multiplier *= getCurrencyMultiplier(today->getCurrency(), currency); + } + return value * multiplier; +} + +bool EntsoeApi::loop() { + if(strlen(token) == 0) + return false; + bool ret = false; + + uint64_t now = millis64(); + + if(midnightMillis == 0) { + time_t epoch = time(nullptr); + + tmElements_t tm; + breakTime(epoch, tm); + if(tm.Year > 50) { // Make sure we are in 2021 or later (years after 1970) + uint64_t curDeviceMillis = millis64(); + uint32_t curDayMillis = (((((tm.Hour * 60) + tm.Minute) * 60) + tm.Second) * 1000); + + printD("Setting midnight millis"); + midnightMillis = curDeviceMillis + (SECS_PER_DAY * 1000) - curDayMillis; + } + } else if(now > midnightMillis) { + printD("Rotating price objects"); + delete today; + today = tomorrow; + tomorrow = NULL; + midnightMillis = 0; // Force new midnight millis calculation + } else { + if(today == NULL) { + time_t e1 = time(nullptr) - (SECS_PER_DAY * 1); + time_t e2 = e1 + SECS_PER_DAY; + tmElements_t d1, d2; + breakTime(e1, d1); + breakTime(e2, d2); + + char url[256]; + snprintf(url, sizeof(url), "%s?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s", + "https://transparency.entsoe.eu/api", token, + d1.Year+1970, d1.Month, d1.Day, 23, 00, + d2.Year+1970, d2.Month, d2.Day, 23, 00, + area, area); + + printD("Fetching prices for today"); + printD(url); + EntsoeA44Parser* a44 = new EntsoeA44Parser(); + if(retrieve(url, a44)) { + today = a44; + ret = true; + } else { + delete a44; + today = NULL; + } + } + + if(tomorrow == NULL + && midnightMillis - now < 43200000 + && (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 3600000) + ) { + time_t e1 = time(nullptr); + time_t e2 = e1 + SECS_PER_DAY; + tmElements_t d1, d2; + breakTime(e1, d1); + breakTime(e2, d2); + + char url[256]; + snprintf(url, sizeof(url), "%s?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s", + "https://transparency.entsoe.eu/api", token, + d1.Year+1970, d1.Month, d1.Day, 23, 00, + d2.Year+1970, d2.Month, d2.Day, 23, 00, + area, area); + + printD("Fetching prices for tomorrow"); + printD(url); + EntsoeA44Parser* a44 = new EntsoeA44Parser(); + if(retrieve(url, a44)) { + tomorrow = a44; + ret = true; + } else { + delete a44; + tomorrow = NULL; + } + lastTomorrowFetch = now; + } + } + return ret; +} + +bool EntsoeApi::retrieve(const char* url, Stream* doc) { + WiFiClientSecure client; +#if defined(ESP8266) + client.setBufferSizes(512, 512); + client.setInsecure(); +#endif + HTTPClient https; +#if defined(ESP8266) + https.setFollowRedirects(true); +#endif + + if(https.begin(client, url)) { + int status = https.GET(); + if(status == HTTP_CODE_OK) { + https.writeToStream(doc); + return true; + } else { + printE("Communication error: "); + printE(https.errorToString(status)); + printI(url); + printD(https.getString()); + return false; + } + } else { + return false; + } +} + +double EntsoeApi::getCurrencyMultiplier(const char* from, const char* to) { + if(strcmp(from, to) == 0) + return 1.00; + + uint64_t now = millis64(); + if(lastCurrencyFetch == 0 || now - lastCurrencyFetch > (SECS_PER_HOUR * 1000)) { + WiFiClientSecure client; + #if defined(ESP8266) + client.setBufferSizes(512, 512); + client.setInsecure(); + #endif + HTTPClient https; + #if defined(ESP8266) + https.setFollowRedirects(true); + #endif + + char url[256]; + snprintf(url, sizeof(url), "https://data.norges-bank.no/api/data/EXR/M.%s.%s.SP?lastNObservations=1", + from, + to + ); + + if(https.begin(client, url)) { + int status = https.GET(); + if(status == HTTP_CODE_OK) { + DnbCurrParser p; + https.writeToStream(&p); + currencyMultiplier = p.getValue(); + } else { + printE("Communication error: "); + printE(https.errorToString(status)); + printI(url); + printD(https.getString()); + } + lastCurrencyFetch = now; + } else { + return false; + } + } + return currencyMultiplier; +} + +void EntsoeApi::printD(String fmt, ...) { + va_list args; + va_start(args, fmt); + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args); + va_end(args); +} + +void EntsoeApi::printI(String fmt, ...) { + va_list args; + va_start(args, fmt); + if(debugger->isActive(RemoteDebug::INFO)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args); + va_end(args); +} + +void EntsoeApi::printW(String fmt, ...) { + va_list args; + va_start(args, fmt); + if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args); + va_end(args); +} + +void EntsoeApi::printE(String fmt, ...) { + va_list args; + va_start(args, fmt); + if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args); + va_end(args); +} diff --git a/src/entsoe/EntsoeApi.h b/src/entsoe/EntsoeApi.h new file mode 100644 index 00000000..4e790ced --- /dev/null +++ b/src/entsoe/EntsoeApi.h @@ -0,0 +1,50 @@ +#ifndef _ENTSOEAPI_H +#define _ENTSOEAPI_H + +#include "time.h" +#include "Timezone.h" +#include "RemoteDebug.h" +#include "EntsoeA44Parser.h" + +#define ENTSOE_NO_VALUE -127 +#define ENTSOE_DEFAULT_MULTIPLIER 1.00 + +class EntsoeApi { +public: + EntsoeApi(RemoteDebug* Debug); + bool loop(); + + double getValueForHour(int hour); + char* getCurrency(); + + void setToken(const char* token); + void setArea(const char* area); + void setCurrency(const char* currency); + void setMultiplier(double multiplier); + +private: + RemoteDebug* debugger; + char token[37]; // UUID + null terminator + + uint64_t midnightMillis = 0; + uint64_t lastTomorrowFetch = 0; + uint64_t lastCurrencyFetch = 0; + EntsoeA44Parser* today = NULL; + EntsoeA44Parser* tomorrow = NULL; + + Timezone* tz = NULL; + + char area[32]; + char currency[4]; + double multiplier = ENTSOE_DEFAULT_MULTIPLIER; + double currencyMultiplier = ENTSOE_DEFAULT_MULTIPLIER; + + bool retrieve(const char* url, Stream* doc); + double getCurrencyMultiplier(const char* from, const char* to); + + void printD(String fmt, ...); + void printI(String fmt, ...); + void printW(String fmt, ...); + void printE(String fmt, ...); +}; +#endif diff --git a/src/web/AmsWebServer.cpp b/src/web/AmsWebServer.cpp index 7fcb3ae9..07e49b83 100644 --- a/src/web/AmsWebServer.cpp +++ b/src/web/AmsWebServer.cpp @@ -12,6 +12,7 @@ #include "root/configmqtt_html.h" #include "root/configweb_html.h" #include "root/configdomoticz_html.h" +#include "root/entsoe_html.h" #include "root/ntp_html.h" #include "root/gpio_html.h" #include "root/debugging_html.h" @@ -23,12 +24,14 @@ #include "root/delete_html.h" #include "root/reset_html.h" #include "root/temperature_html.h" +#include "root/price_html.h" #include "base64.h" -AmsWebServer::AmsWebServer(RemoteDebug* Debug, HwTools* hw) { +AmsWebServer::AmsWebServer(RemoteDebug* Debug, HwTools* hw, EntsoeApi* eapi) { this->debugger = Debug; this->hw = hw; + this->eapi = eapi; } void AmsWebServer::setup(AmsConfiguration* config, MQTTClient* mqtt) { @@ -41,11 +44,13 @@ void AmsWebServer::setup(AmsConfiguration* config, MQTTClient* mqtt) { server.on("/temperature", HTTP_GET, std::bind(&AmsWebServer::temperature, this)); server.on("/temperature", HTTP_POST, std::bind(&AmsWebServer::temperaturePost, this)); server.on("/temperature.json", HTTP_GET, std::bind(&AmsWebServer::temperatureJson, this)); + server.on("/price", HTTP_GET, std::bind(&AmsWebServer::price, this)); server.on("/config-meter", HTTP_GET, std::bind(&AmsWebServer::configMeterHtml, this)); server.on("/config-wifi", HTTP_GET, std::bind(&AmsWebServer::configWifiHtml, this)); server.on("/config-mqtt", HTTP_GET, std::bind(&AmsWebServer::configMqttHtml, this)); server.on("/config-web", HTTP_GET, std::bind(&AmsWebServer::configWebHtml, this)); server.on("/config-domoticz",HTTP_GET, std::bind(&AmsWebServer::configDomoticzHtml, this)); + server.on("/config-entsoe",HTTP_GET, std::bind(&AmsWebServer::configEntsoeHtml, this)); server.on("/boot.css", HTTP_GET, std::bind(&AmsWebServer::bootCss, this)); server.on("/gaugemeter.js", HTTP_GET, std::bind(&AmsWebServer::gaugemeterJs, this)); server.on("/github.svg", HTTP_GET, std::bind(&AmsWebServer::githubSvg, this)); @@ -74,6 +79,10 @@ void AmsWebServer::setup(AmsConfiguration* config, MQTTClient* mqtt) { server.on("/reset", HTTP_POST, std::bind(&AmsWebServer::factoryResetPost, this)); server.onNotFound(std::bind(&AmsWebServer::notFound, this)); + + TimeChangeRule STD = {"STD", Last, Sun, Oct, 3, config->getNtpOffset() / 60}; + TimeChangeRule DST = {"DST", Last, Sun, Mar, 2, (config->getNtpOffset() + config->getNtpSummerOffset()) / 60}; + tz = new Timezone(DST, STD); server.begin(); // Web server start } @@ -127,6 +136,9 @@ bool AmsWebServer::checkSecurity(byte level) { void AmsWebServer::temperature() { printD("Serving /temperature.html over http..."); + if(!checkSecurity(2)) + return; + server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "-1"); @@ -138,6 +150,9 @@ void AmsWebServer::temperature() { } void AmsWebServer::temperaturePost() { + if(!checkSecurity(1)) + return; + printD("Saving temperature sensors..."); for(int i = 0; i < 32; i++) { if(!server.hasArg("sensor" + String(i, DEC))) break; @@ -200,6 +215,38 @@ void AmsWebServer::temperatureJson() { server.send(200, "application/json", jsonStr); } +void AmsWebServer::price() { + printD("Serving /price.html over http..."); + + if(!checkSecurity(2)) + return; + + server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + server.sendHeader("Pragma", "no-cache"); + server.sendHeader("Expires", "-1"); + + String html = String((const __FlashStringHelper*) PRICE_HTML); + for(int i = 0; i < 24; i++) { + tmElements_t tm; + breakTime(tz->toLocal(time(nullptr)) + (SECS_PER_HOUR * i), tm); + char ts[5]; + sprintf(ts, "%02d:00", tm.Hour); + html.replace("${time" + String(i) + "}", String(ts)); + + double price = eapi->getValueForHour(i); + if(price == ENTSOE_NO_VALUE) { + html.replace("${price" + String(i) + "}", "--"); + } else { + html.replace("${price" + String(i) + "}", String(price, 4)); + } + } + + server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN); + server.send_P(200, "text/html", HEAD_HTML); + server.sendContent(html); + server.sendContent_P(FOOT_HTML); +} + void AmsWebServer::indexHtml() { printD("Serving /index.html over http..."); @@ -441,10 +488,6 @@ void AmsWebServer::configDomoticzHtml() { String html = String((const __FlashStringHelper*) CONFIGDOMOTICZ_HTML); - if(WiFi.getMode() != WIFI_AP) { - html.replace("boot.css", BOOTSTRAP_URL); - } - server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); server.sendHeader("Pragma", "no-cache"); @@ -465,6 +508,41 @@ void AmsWebServer::configDomoticzHtml() { server.sendContent_P(FOOT_HTML); } +void AmsWebServer::configEntsoeHtml() { + printD("Serving /config-entsoe.html over http..."); + + if(!checkSecurity(1)) + return; + + String html = String((const __FlashStringHelper*) ENTSOE_HTML); + + html.replace("${config.entsoeApiToken}", config->getEntsoeApiToken()); + html.replace("${config.entsoeApiMultiplier}", String(config->getEntsoeApiMultiplier(), 3)); + + html.replace("${config.entsoeApiAreaNo1}", strcmp(config->getEntsoeApiArea(), "10YNO-1--------2") == 0 ? "selected" : ""); + html.replace("${config.entsoeApiAreaNo2}", strcmp(config->getEntsoeApiArea(), "10YNO-2--------T") == 0 ? "selected" : ""); + html.replace("${config.entsoeApiAreaNo3}", strcmp(config->getEntsoeApiArea(), "10YNO-3--------J") == 0 ? "selected" : ""); + html.replace("${config.entsoeApiAreaNo4}", strcmp(config->getEntsoeApiArea(), "10YNO-4--------9") == 0 ? "selected" : ""); + html.replace("${config.entsoeApiAreaNo5}", strcmp(config->getEntsoeApiArea(), "10Y1001A1001A48H") == 0 ? "selected" : ""); + + html.replace("${config.entsoeApiAreaSe1}", strcmp(config->getEntsoeApiArea(), "10Y1001A1001A44P") == 0 ? "selected" : ""); + html.replace("${config.entsoeApiAreaSe2}", strcmp(config->getEntsoeApiArea(), "10Y1001A1001A45N") == 0 ? "selected" : ""); + html.replace("${config.entsoeApiAreaSe3}", strcmp(config->getEntsoeApiArea(), "10Y1001A1001A46L") == 0 ? "selected" : ""); + html.replace("${config.entsoeApiAreaSe4}", strcmp(config->getEntsoeApiArea(), "10Y1001A1001A47J") == 0 ? "selected" : ""); + + html.replace("${config.entsoeApiAreaDk1}", strcmp(config->getEntsoeApiArea(), "10YDK-1--------W") == 0 ? "selected" : ""); + html.replace("${config.entsoeApiAreaDk2}", strcmp(config->getEntsoeApiArea(), "10YDK-2--------M") == 0 ? "selected" : ""); + + html.replace("${config.entsoeApiCurrencyNOK}", strcmp(config->getEntsoeApiArea(), "NOK") == 0 ? "selected" : ""); + html.replace("${config.entsoeApiCurrencySEK}", strcmp(config->getEntsoeApiArea(), "SEK") == 0 ? "selected" : ""); + html.replace("${config.entsoeApiCurrencyDKK}", strcmp(config->getEntsoeApiArea(), "DKK") == 0 ? "selected" : ""); + html.replace("${config.entsoeApiCurrencyEUR}", strcmp(config->getEntsoeApiArea(), "EUR") == 0 ? "selected" : ""); + + server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN); + server.send_P(200, "text/html", HEAD_HTML); + server.sendContent(html); + server.sendContent_P(FOOT_HTML); +} void AmsWebServer::configWebHtml() { printD("Serving /config-web.html over http..."); @@ -773,6 +851,10 @@ void AmsWebServer::handleSetup() { } void AmsWebServer::handleSave() { + printD("Handling save method from http"); + if(!checkSecurity(1)) + return; + String temp; if(server.hasArg("meterConfig") && server.arg("meterConfig") == "true") { @@ -790,7 +872,6 @@ void AmsWebServer::handleSave() { fromHex(hexStr, encryptionKeyHex, 16); config->setMeterEncryptionKey(hexStr); } - printD("Meter 8"); String authenticationKeyHex = server.arg("meterAuthenticationKey"); if(!authenticationKeyHex.isEmpty()) { @@ -919,6 +1000,13 @@ void AmsWebServer::handleSave() { config->setNtpServer(server.arg("ntpServer").c_str()); } + if(server.hasArg("entsoeConfig") && server.arg("entsoeConfig") == "true") { + config->setEntsoeApiToken(server.arg("entsoeApiToken").c_str()); + config->setEntsoeApiArea(server.arg("entsoeApiArea").c_str()); + config->setEntsoeApiCurrency(server.arg("entsoeApiCurrency").c_str()); + config->setEntsoeApiMultiplier(server.arg("entsoeApiMultiplier").toDouble()); + } + printI("Saving configuration now..."); if (debugger->isActive(RemoteDebug::DEBUG)) config->print(debugger); @@ -939,6 +1027,11 @@ void AmsWebServer::handleSave() { hw->setVccPin(config->getVccPin()); hw->setVccOffset(config->getVccOffset()); hw->setVccMultiplier(config->getVccMultiplier()); + + eapi->setToken(config->getEntsoeApiToken()); + eapi->setArea(config->getEntsoeApiArea()); + eapi->setCurrency(config->getEntsoeApiCurrency()); + eapi->setMultiplier(config->getEntsoeApiMultiplier()); } } else { printE("Error saving configuration"); @@ -1124,10 +1217,16 @@ void AmsWebServer::deleteFile(const char* path) { void AmsWebServer::firmwareHtml() { printD("Serving /firmware.html over http..."); + if(!checkSecurity(1)) + return; + uploadHtml("CA file", "/firmware", "mqtt"); } void AmsWebServer::firmwareUpload() { + if(!checkSecurity(1)) + return; + HTTPUpload& upload = server.upload(); if(upload.status == UPLOAD_FILE_START) { String filename = upload.filename; @@ -1146,6 +1245,9 @@ void AmsWebServer::firmwareUpload() { const uint8_t githubFingerprint[] = {0x59, 0x74, 0x61, 0x88, 0x13, 0xCA, 0x12, 0x34, 0x15, 0x4D, 0x11, 0x0A, 0xC1, 0x7F, 0xE6, 0x67, 0x07, 0x69, 0x42, 0xF5}; void AmsWebServer::firmwareDownload() { + if(!checkSecurity(1)) + return; + printD("Firmware download URL triggered"); if(server.hasArg("version")) { String version = server.arg("version"); @@ -1275,6 +1377,9 @@ void AmsWebServer::deleteHtml(const char* label, const char* action, const char* void AmsWebServer::mqttCa() { printD("Serving /mqtt-ca.html over http..."); + if(!checkSecurity(1)) + return; + if(SPIFFS.begin()) { if(SPIFFS.exists(FILE_MQTT_CA)) { deleteHtml("CA file", "/mqtt-ca/delete", "mqtt"); @@ -1289,6 +1394,9 @@ void AmsWebServer::mqttCa() { } void AmsWebServer::mqttCaUpload() { + if(!checkSecurity(1)) + return; + uploadFile(FILE_MQTT_CA); HTTPUpload& upload = server.upload(); if(upload.status == UPLOAD_FILE_END) { @@ -1301,6 +1409,9 @@ void AmsWebServer::mqttCaUpload() { } void AmsWebServer::mqttCaDelete() { + if(!checkSecurity(1)) + return; + if(!uploading) { // Not an upload deleteFile(FILE_MQTT_CA); server.sendHeader("Location","/config-mqtt"); @@ -1317,6 +1428,9 @@ void AmsWebServer::mqttCaDelete() { void AmsWebServer::mqttCert() { printD("Serving /mqtt-cert.html over http..."); + if(!checkSecurity(1)) + return; + if(SPIFFS.begin()) { if(SPIFFS.exists(FILE_MQTT_CERT)) { deleteHtml("Certificate", "/mqtt-cert/delete", "mqtt"); @@ -1331,6 +1445,9 @@ void AmsWebServer::mqttCert() { } void AmsWebServer::mqttCertUpload() { + if(!checkSecurity(1)) + return; + uploadFile(FILE_MQTT_CERT); HTTPUpload& upload = server.upload(); if(upload.status == UPLOAD_FILE_END) { @@ -1343,6 +1460,9 @@ void AmsWebServer::mqttCertUpload() { } void AmsWebServer::mqttCertDelete() { + if(!checkSecurity(1)) + return; + if(!uploading) { // Not an upload deleteFile(FILE_MQTT_CERT); server.sendHeader("Location","/config-mqtt"); @@ -1359,6 +1479,9 @@ void AmsWebServer::mqttCertDelete() { void AmsWebServer::mqttKey() { printD("Serving /mqtt-key.html over http..."); + if(!checkSecurity(1)) + return; + if(SPIFFS.begin()) { if(SPIFFS.exists(FILE_MQTT_KEY)) { deleteHtml("Private key", "/mqtt-key/delete", "mqtt"); @@ -1373,6 +1496,9 @@ void AmsWebServer::mqttKey() { } void AmsWebServer::mqttKeyUpload() { + if(!checkSecurity(1)) + return; + uploadFile(FILE_MQTT_KEY); HTTPUpload& upload = server.upload(); if(upload.status == UPLOAD_FILE_END) { @@ -1385,6 +1511,9 @@ void AmsWebServer::mqttKeyUpload() { } void AmsWebServer::mqttKeyDelete() { + if(!checkSecurity(1)) + return; + if(!uploading) { // Not an upload deleteFile(FILE_MQTT_KEY); server.sendHeader("Location","/config-mqtt"); @@ -1399,6 +1528,9 @@ void AmsWebServer::mqttKeyDelete() { } void AmsWebServer::factoryResetHtml() { + if(!checkSecurity(1)) + return; + server.sendHeader("Cache-Control", "public, max-age=3600"); server.setContentLength(RESET_HTML_LEN + HEAD_HTML_LEN + FOOT_HTML_LEN); @@ -1408,6 +1540,9 @@ void AmsWebServer::factoryResetHtml() { } void AmsWebServer::factoryResetPost() { + if(!checkSecurity(1)) + return; + printD("Performing factory reset"); if(server.hasArg("perform") && server.arg("perform") == "true") { printD("Formatting SPIFFS"); diff --git a/src/web/AmsWebServer.h b/src/web/AmsWebServer.h index 8221554a..fa6c62cd 100644 --- a/src/web/AmsWebServer.h +++ b/src/web/AmsWebServer.h @@ -10,6 +10,7 @@ #include "AmsData.h" #include "Uptime.h" #include "RemoteDebug.h" +#include "entsoe/EntsoeApi.h" #if defined(ARDUINO) && ARDUINO >= 100 #include "Arduino.h" @@ -33,7 +34,7 @@ class AmsWebServer { public: - AmsWebServer(RemoteDebug* Debug, HwTools* hw); + AmsWebServer(RemoteDebug* Debug, HwTools* hw, EntsoeApi* eapi); void setup(AmsConfiguration* config, MQTTClient* mqtt); void loop(); @@ -43,6 +44,8 @@ private: RemoteDebug* debugger; int maxPwr = 0; HwTools* hw; + Timezone* tz; + EntsoeApi* eapi; AmsConfiguration* config; AmsData data; MQTTClient* mqtt; @@ -63,11 +66,13 @@ private: void temperature(); void temperaturePost(); void temperatureJson(); + void price(); void configMeterHtml(); void configWifiHtml(); void configMqttHtml(); void configWebHtml(); void configDomoticzHtml(); + void configEntsoeHtml(); void configNtpHtml(); void configGpioHtml(); void configDebugHtml(); diff --git a/web/application.js b/web/application.js index f320fd74..3cd59039 100644 --- a/web/application.js +++ b/web/application.js @@ -111,19 +111,18 @@ $(function() { case '/temperature': $('#config-temp-link').addClass('active'); break; - case '/config-meter': + case '/price': + $('#config-price-link').addClass('active'); + break; + case '/config-meter': $('#config-meter-link').addClass('active'); break; - case '/config-wifi': - $('#config-wifi-link').addClass('active'); - break; + case '/config-wifi': case '/config-mqtt': case '/mqtt-ca': case '/mqtt-cert': case '/mqtt-key': case '/config-domoticz': - $('#config-mqtt-link').addClass('active'); - break; case '/config-web': case '/ntp': case '/gpio': diff --git a/web/configdomoticz.html b/web/configdomoticz.html index 6dec843f..733b617c 100644 --- a/web/configdomoticz.html +++ b/web/configdomoticz.html @@ -1,17 +1,5 @@
-
-

Domoticz Configuration. Requires that a Domoticz MQTT-message-broker is setup. HOWTO: https://www.domoticz.com/wiki/MQTT.

-

The following virtual sensors can currently be used:

-
    -
  • Electricity (instant and counter)
  • -
  • Electricity Current/Ampere 3 Phase
  • -
  • Voltage
  • -
-

see: https://www.domoticz.com/wiki/Domoticz_API/JSON_URL's

-

Create the sensors in Domoticz under Hardware > Dummy > Create virtual sensor, and use the IDX assigned to the sensor as input here.

-

"Electricity (instant and counter)" relies on Total energy import "tPI" and will not start before the first value is read (once an hour).

-
diff --git a/web/configmqtt.html b/web/configmqtt.html index 00308b23..ac9fc996 100644 --- a/web/configmqtt.html +++ b/web/configmqtt.html @@ -1,6 +1,7 @@
+
MQTT
diff --git a/web/configweb.html b/web/configweb.html index 89fbf1de..38df0106 100644 --- a/web/configweb.html +++ b/web/configweb.html @@ -1,6 +1,7 @@
+
Web
diff --git a/web/configwifi.html b/web/configwifi.html index f435a714..a0371105 100644 --- a/web/configwifi.html +++ b/web/configwifi.html @@ -1,6 +1,7 @@
+
WiFi
diff --git a/web/entsoe.html b/web/entsoe.html new file mode 100644 index 00000000..f92dfe4a --- /dev/null +++ b/web/entsoe.html @@ -0,0 +1,72 @@ + + +
+
ENTSO-E API
+
+
+
+
+ Token +
+ +
+
+
+
+
+ Region +
+ +
+
+
+
+
+ Currency +
+ +
+
+
+
+
+ Multiplier +
+ +
+
+
+
+
+
+
+ Back +
+
+ +
+
+ diff --git a/web/head.html b/web/head.html index 05b0d154..8fc17e87 100644 --- a/web/head.html +++ b/web/head.html @@ -56,26 +56,27 @@ + - -