Added support for retrieving energy price from ENTSO-E API

This commit is contained in:
Gunnar Skjold
2021-01-10 20:54:25 +01:00
parent 402ecf67d7
commit f2dda26bbb
19 changed files with 1252 additions and 129 deletions

View File

@@ -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;

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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, "<Obs", 4) == 0) {
for(int i = 0; i < pos; i++) {
if(strncmp(buf+i, "OBS_VALUE=\"", 11) == 0) {
pos = i + 11;
break;
}
}
for(int i = 0; i < 16; i++) {
uint8_t b = buf[pos+i];
if(b == '"') {
buf[pos+i] = '\0';
break;
}
}
value = String(buf+pos).toDouble();
}
pos = 0;
} else {
buf[pos++] = byte;
}
return 1;
}

View File

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

View File

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

View File

@@ -0,0 +1,38 @@
#ifndef _ENTSOEA44PARSER_H
#define _ENTSOEA44PARSER_H
#include "Stream.h"
#define DOCPOS_SEEK 0
#define DOCPOS_CURRENCY 1
#define DOCPOS_MEASUREMENTUNIT 2
#define DOCPOS_POSITION 3
#define DOCPOS_AMOUNT 4
class EntsoeA44Parser: public Stream {
public:
EntsoeA44Parser();
char* getCurrency();
char* getMeasurementUnit();
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

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

@@ -0,0 +1,261 @@
#include "EntsoeApi.h"
#include <EEPROM.h>
#include "Uptime.h"
#include "Time.h"
#include "DnbCurrParser.h"
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
#include <HTTPClient.h>
#else
#warning "Unsupported board type"
#endif
EntsoeApi::EntsoeApi(RemoteDebug* Debug) {
debugger = Debug;
// Entso-E uses CET/CEST
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
tz = new Timezone(CEST, CET);
}
void EntsoeApi::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);
}

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

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

View File

@@ -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");

View File

@@ -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();