From 5beb13894c9a01100a3a4ed9d3c19ccd3a2dc6ee Mon Sep 17 00:00:00 2001 From: Gunnar Skjold Date: Sat, 9 Jan 2021 11:11:49 +0100 Subject: [PATCH 01/22] Initial implementation of supporting timezone in timestamp --- lib/HanReader/src/HanReader.cpp | 21 ++++++++++++++------- lib/HanReader/src/HanReader.h | 8 ++++---- src/AmsData.cpp | 20 ++++++++++---------- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/lib/HanReader/src/HanReader.cpp b/lib/HanReader/src/HanReader.cpp index 4d18d501..170f16e4 100644 --- a/lib/HanReader/src/HanReader.cpp +++ b/lib/HanReader/src/HanReader.cpp @@ -169,15 +169,15 @@ int HanReader::getListSize() { return listSize; } -time_t HanReader::getPackageTime() { +time_t HanReader::getPackageTime(bool respectTimezone, bool respectDsc) { int packageTimePosition = dataHeader + (compensateFor09HeaderBug ? 1 : 0); - return getTime(buffer, packageTimePosition, bytesRead); + return getTime(buffer, packageTimePosition, bytesRead, respectTimezone, respectDsc); } -time_t HanReader::getTime(int objectId) { - return getTime(objectId, buffer, 0, bytesRead); +time_t HanReader::getTime(int objectId, bool respectTimezone, bool respectDsc) { + return getTime(objectId, respectTimezone, respectDsc, buffer, 0, bytesRead); } int32_t HanReader::getInt(int objectId) { @@ -240,14 +240,14 @@ int HanReader::findValuePosition(int dataPosition, byte *buffer, int start, int } -time_t HanReader::getTime(int dataPosition, byte *buffer, int start, int length) { +time_t HanReader::getTime(int dataPosition, bool respectTimezone, bool respectDsc, byte *buffer, int start, int length) { // TODO: check if the time is represented always as a 12 byte string (0x09 0x0C) int timeStart = findValuePosition(dataPosition, buffer, start, length); timeStart += 1; - return getTime(buffer, start + timeStart, length - timeStart); + return getTime(buffer, start + timeStart, length - timeStart, respectTimezone, respectDsc); } -time_t HanReader::getTime(byte *buffer, int start, int length) { +time_t HanReader::getTime(byte *buffer, int start, int length, bool respectTimezone, bool respectDsc) { int pos = start; int dataLength = buffer[pos++]; @@ -257,9 +257,16 @@ time_t HanReader::getTime(byte *buffer, int start, int length) { int month = buffer[pos + 2]; int day = buffer[pos + 3]; + // 4: Day of week int hour = buffer[pos + 5]; int minute = buffer[pos + 6]; int second = buffer[pos + 7]; + // 8: Hundredths + int tzMinutes = buffer[pos + 9] << 8 | buffer[pos + 10]; + bool dsc = (buffer[pos + 11] & 0x01) == 0x01; + + printD("Time offset: %d", tzMinutes); + printD(dsc ? "DSC" : "not DSC"); tmElements_t tm; tm.Year = year - 1970; diff --git a/lib/HanReader/src/HanReader.h b/lib/HanReader/src/HanReader.h index 980a2d54..f4a4906d 100644 --- a/lib/HanReader/src/HanReader.h +++ b/lib/HanReader/src/HanReader.h @@ -24,11 +24,11 @@ public: bool read(); bool read(byte data); int getListSize(); - time_t getPackageTime(); + time_t getPackageTime(bool respectTimezone, bool respectDsc); int32_t getInt(int objectId); // Use this for uint8, int8, uint16, int16 uint32_t getUint(int objectId); // Only for uint32 String getString(int objectId); - time_t getTime(int objectId); + time_t getTime(int objectId, bool respectTimezone, bool respectDsc); int getBuffer(byte* buf); void setEncryptionKey(uint8_t* encryption_key); @@ -47,8 +47,8 @@ private: int findValuePosition(int dataPosition, byte *buffer, int start, int length); - time_t getTime(int dataPosition, byte *buffer, int start, int length); - time_t getTime(byte *buffer, int start, int length); + time_t getTime(int dataPosition, bool respectTimezone, bool respectDsc, byte *buffer, int start, int length); + time_t getTime(byte *buffer, int start, int length, bool respectTimezone, bool respectDsc); int getInt(int dataPosition, byte *buffer, int start, int length); int8_t getInt8(int dataPosition, byte *buffer, int start, int length); uint8_t getUint8(int dataPosition, byte *buffer, int start, int length); diff --git a/src/AmsData.cpp b/src/AmsData.cpp index 267c3a76..9295af95 100644 --- a/src/AmsData.cpp +++ b/src/AmsData.cpp @@ -8,7 +8,7 @@ AmsData::AmsData() {} AmsData::AmsData(int meterType, bool substituteMissing, HanReader& hanReader) { lastUpdateMillis = millis(); - packageTimestamp = hanReader.getPackageTime(); + packageTimestamp = hanReader.getPackageTime(true, true); int listSize = hanReader.getListSize(); switch(meterType) { @@ -49,7 +49,7 @@ void AmsData::extractFromKaifa(HanReader& hanReader, int listSize) { } else { switch(listSize) { case (int)Kaifa::List3PhaseLong: - meterTimestamp = hanReader.getTime( (int)Kaifa_List3Phase::MeterClock); + meterTimestamp = hanReader.getTime( (int)Kaifa_List3Phase::MeterClock, false, false); activeImportCounter = ((double) hanReader.getUint((int)Kaifa_List3Phase::CumulativeActiveImportEnergy)) / 1000; activeExportCounter = ((double) hanReader.getUint((int)Kaifa_List3Phase::CumulativeActiveExportEnergy)) / 1000; reactiveImportCounter = ((double) hanReader.getUint((int)Kaifa_List3Phase::CumulativeReactiveImportEnergy)) / 1000; @@ -70,7 +70,7 @@ void AmsData::extractFromKaifa(HanReader& hanReader, int listSize) { l3voltage = ((double) hanReader.getInt( (int)Kaifa_List3Phase::VoltageL3)) / 10; break; case (int)Kaifa::List1PhaseLong: - meterTimestamp = hanReader.getTime( (int)Kaifa_List1Phase::MeterClock); + meterTimestamp = hanReader.getTime( (int)Kaifa_List1Phase::MeterClock, false, false); activeImportCounter = ((double) hanReader.getUint((int)Kaifa_List1Phase::CumulativeActiveImportEnergy)); activeExportCounter = ((double) hanReader.getUint((int)Kaifa_List1Phase::CumulativeActiveExportEnergy)); reactiveImportCounter = ((double) hanReader.getUint((int)Kaifa_List1Phase::CumulativeReactiveImportEnergy)); @@ -114,7 +114,7 @@ void AmsData::extractFromAidon(HanReader& hanReader, int listSize, bool substitu } else { switch(listSize) { case (int)Aidon::List3PhaseLong: - meterTimestamp = hanReader.getTime( (int)Aidon_List3Phase::Timestamp); + meterTimestamp = hanReader.getTime( (int)Aidon_List3Phase::Timestamp, false, false); activeImportCounter = ((double) hanReader.getUint( (int)Aidon_List3Phase::CumulativeActiveImportEnergy)) / 100; activeExportCounter = ((double) hanReader.getUint( (int)Aidon_List3Phase::CumulativeActiveExportEnergy)) / 100; reactiveImportCounter = ((double) hanReader.getUint( (int)Aidon_List3Phase::CumulativeReactiveImportEnergy)) / 100; @@ -135,7 +135,7 @@ void AmsData::extractFromAidon(HanReader& hanReader, int listSize, bool substitu l3voltage = ((double) hanReader.getInt( (int)Aidon_List3Phase::VoltageL3)) / 10; break; case (int)Aidon::List1PhaseLong: - meterTimestamp = hanReader.getTime( (int)Aidon_List1Phase::Timestamp); + meterTimestamp = hanReader.getTime( (int)Aidon_List1Phase::Timestamp, false, false); activeImportCounter = ((double) hanReader.getUint( (int)Aidon_List1Phase::CumulativeActiveImportEnergy)) / 100; activeExportCounter = ((double) hanReader.getUint( (int)Aidon_List1Phase::CumulativeActiveExportEnergy)) / 100; reactiveImportCounter = ((double) hanReader.getUint( (int)Aidon_List1Phase::CumulativeReactiveImportEnergy)) / 100; @@ -152,7 +152,7 @@ void AmsData::extractFromAidon(HanReader& hanReader, int listSize, bool substitu l1voltage = ((double) hanReader.getInt( (int)Aidon_List1Phase::VoltageL1)) / 10; break; case (int)Aidon::List3PhaseITLong: - meterTimestamp = hanReader.getTime( (int)Aidon_List3PhaseIT::Timestamp); + meterTimestamp = hanReader.getTime( (int)Aidon_List3PhaseIT::Timestamp, false, false); activeImportCounter = ((double) hanReader.getUint( (int)Aidon_List3PhaseIT::CumulativeActiveImportEnergy)) / 100; activeExportCounter = ((double) hanReader.getUint( (int)Aidon_List3PhaseIT::CumulativeActiveExportEnergy)) / 100; reactiveImportCounter = ((double) hanReader.getUint( (int)Aidon_List3PhaseIT::CumulativeReactiveImportEnergy)) / 100; @@ -197,7 +197,7 @@ void AmsData::extractFromKamstrup(HanReader& hanReader, int listSize, bool subst switch(listSize) { case (int)Kamstrup::List1PhaseLong: - meterTimestamp = hanReader.getTime( (int)Kamstrup_List1Phase::MeterClock); + meterTimestamp = hanReader.getTime( (int)Kamstrup_List1Phase::MeterClock, true, true); activeImportCounter = ((double) hanReader.getInt((int)Kamstrup_List1Phase::CumulativeActiveImportEnergy)) / 100; activeExportCounter = ((double) hanReader.getInt((int)Kamstrup_List1Phase::CumulativeActiveExportEnergy)) / 100; reactiveImportCounter = ((double) hanReader.getInt((int)Kamstrup_List1Phase::CumulativeReactiveImportEnergy)) / 100; @@ -214,7 +214,7 @@ void AmsData::extractFromKamstrup(HanReader& hanReader, int listSize, bool subst l1voltage = hanReader.getInt( (int)Kamstrup_List1Phase::VoltageL1); break; case (int)Kamstrup::List3PhaseLong: - meterTimestamp = hanReader.getTime( (int)Kamstrup_List3Phase::MeterClock); + meterTimestamp = hanReader.getTime( (int)Kamstrup_List3Phase::MeterClock, true, true); activeImportCounter = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CumulativeActiveImportEnergy)) / 100; activeExportCounter = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CumulativeActiveExportEnergy)) / 100; reactiveImportCounter = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CumulativeReactiveImportEnergy)) / 100; @@ -235,7 +235,7 @@ void AmsData::extractFromKamstrup(HanReader& hanReader, int listSize, bool subst l3voltage = hanReader.getInt( (int)Kamstrup_List3Phase::VoltageL3); break; case (int)Kamstrup::List3PhaseITLong: - meterTimestamp = hanReader.getTime( (int)Kamstrup_List3Phase::MeterClock); + meterTimestamp = hanReader.getTime( (int)Kamstrup_List3Phase::MeterClock, true, true); activeImportCounter = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CumulativeActiveImportEnergy)) / 100; activeExportCounter = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CumulativeActiveExportEnergy)) / 100; reactiveImportCounter = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CumulativeReactiveImportEnergy)) / 100; @@ -264,7 +264,7 @@ void AmsData::extractFromKamstrup(HanReader& hanReader, int listSize, bool subst void AmsData::extractFromOmnipower(HanReader& hanReader, int listSize) { switch(listSize) { case (int)Omnipower::DLMS: - meterTimestamp = hanReader.getTime( (int)Omnipower_DLMS::MeterClock); + meterTimestamp = hanReader.getTime( (int)Omnipower_DLMS::MeterClock, true, true); activeImportCounter = ((double) hanReader.getInt((int)Omnipower_DLMS::CumulativeActiveImportEnergy)) / 100; activeExportCounter = ((double) hanReader.getInt((int)Omnipower_DLMS::CumulativeActiveExportEnergy)) / 100; reactiveImportCounter = ((double) hanReader.getInt((int)Omnipower_DLMS::CumulativeReactiveImportEnergy)) / 100; From f2dda26bbbca146c35ca92a7b6734c3f00d0d5cc Mon Sep 17 00:00:00 2001 From: Gunnar Skjold Date: Sun, 10 Jan 2021 20:54:25 +0100 Subject: [PATCH 02/22] 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 @@ + - - From 88528b4099adc0af259a9018a5b6f5b49b541fb1 Mon Sep 17 00:00:00 2001 From: Gunnar Skjold Date: Sun, 10 Jan 2021 22:30:55 +0100 Subject: [PATCH 04/22] Added reboot and cleaned up a bit in UI --- src/web/AmsWebServer.cpp | 30 +++++++++++++++++++++++++++++- src/web/AmsWebServer.h | 2 ++ web/application.js | 9 +++++---- web/configmeter.html | 1 + web/head.html | 33 ++++++++++++++++++++------------- web/restart.html | 14 ++++++++++++++ 6 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 web/restart.html diff --git a/src/web/AmsWebServer.cpp b/src/web/AmsWebServer.cpp index 07e49b83..fd8afbd1 100644 --- a/src/web/AmsWebServer.cpp +++ b/src/web/AmsWebServer.cpp @@ -16,6 +16,7 @@ #include "root/ntp_html.h" #include "root/gpio_html.h" #include "root/debugging_html.h" +#include "root/restart_html.h" #include "root/restartwait_html.h" #include "root/boot_css.h" #include "root/gaugemeter_js.h" @@ -50,7 +51,7 @@ void AmsWebServer::setup(AmsConfiguration* config, MQTTClient* mqtt) { 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("/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)); @@ -65,6 +66,8 @@ void AmsWebServer::setup(AmsConfiguration* config, MQTTClient* mqtt) { server.on("/firmware", HTTP_GET, std::bind(&AmsWebServer::firmwareHtml, this)); server.on("/firmware", HTTP_POST, std::bind(&AmsWebServer::uploadPost, this), std::bind(&AmsWebServer::firmwareUpload, this)); server.on("/upgrade", HTTP_GET, std::bind(&AmsWebServer::firmwareDownload, this)); + server.on("/restart", HTTP_GET, std::bind(&AmsWebServer::restartHtml, this)); + server.on("/restart", HTTP_POST, std::bind(&AmsWebServer::restartPost, this)); server.on("/restart-wait", HTTP_GET, std::bind(&AmsWebServer::restartWaitHtml, this)); server.on("/is-alive", HTTP_GET, std::bind(&AmsWebServer::isAliveCheck, this)); @@ -1311,6 +1314,31 @@ void AmsWebServer::firmwareDownload() { } } +void AmsWebServer::restartHtml() { + printD("Serving /restart.html over http..."); + + if(!checkSecurity(1)) + return; + + server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + server.sendHeader("Pragma", "no-cache"); + + server.setContentLength(RESTART_HTML_LEN + HEAD_HTML_LEN + FOOT_HTML_LEN); + server.send_P(200, "text/html", HEAD_HTML); + server.sendContent_P(RESTART_HTML); + server.sendContent_P(FOOT_HTML); +} + +void AmsWebServer::restartPost() { + if(!checkSecurity(1)) + return; + + printD("Setting restart flag and redirecting"); + performRestart = true; + server.sendHeader("Location","/restart-wait"); + server.send(303); +} + void AmsWebServer::restartWaitHtml() { printD("Serving /restart-wait.html over http..."); diff --git a/src/web/AmsWebServer.h b/src/web/AmsWebServer.h index fa6c62cd..6e45c6ec 100644 --- a/src/web/AmsWebServer.h +++ b/src/web/AmsWebServer.h @@ -88,6 +88,8 @@ private: void firmwareHtml(); void firmwareUpload(); void firmwareDownload(); + void restartHtml(); + void restartPost(); void restartWaitHtml(); void isAliveCheck(); diff --git a/web/application.js b/web/application.js index 3cd59039..a3732eb4 100644 --- a/web/application.js +++ b/web/application.js @@ -114,9 +114,7 @@ $(function() { case '/price': $('#config-price-link').addClass('active'); break; - case '/config-meter': - $('#config-meter-link').addClass('active'); - break; + case '/config-meter': case '/config-wifi': case '/config-mqtt': case '/mqtt-ca': @@ -125,11 +123,14 @@ $(function() { case '/config-domoticz': case '/config-web': case '/ntp': + case '/entsoe': + $('#config-link').addClass('active'); + break; case '/gpio': case '/debugging': case '/firmware': case '/reset': - $('#config-system-link').addClass('active'); + $('#system-link').addClass('active'); break; } diff --git a/web/configmeter.html b/web/configmeter.html index 84712e04..5f7fd0db 100644 --- a/web/configmeter.html +++ b/web/configmeter.html @@ -1,6 +1,7 @@
+
Meter
diff --git a/web/head.html b/web/head.html index 64273661..2b2fd101 100644 --- a/web/head.html +++ b/web/head.html @@ -59,32 +59,39 @@ - +
diff --git a/web/restart.html b/web/restart.html new file mode 100644 index 00000000..fb5c277d --- /dev/null +++ b/web/restart.html @@ -0,0 +1,14 @@ + +
+
Are you sure you want restart?
+
+
+
+
+ Back +
+
+ +
+
+ From 75956c087c485c4f1a84f6db489eb8d89f323414 Mon Sep 17 00:00:00 2001 From: Gunnar Skjold Date: Mon, 11 Jan 2021 09:49:25 +0100 Subject: [PATCH 05/22] Closing http connections after receiving data --- src/entsoe/EntsoeApi.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/entsoe/EntsoeApi.cpp b/src/entsoe/EntsoeApi.cpp index 3ec8dcdb..08058376 100644 --- a/src/entsoe/EntsoeApi.cpp +++ b/src/entsoe/EntsoeApi.cpp @@ -177,12 +177,14 @@ bool EntsoeApi::retrieve(const char* url, Stream* doc) { int status = https.GET(); if(status == HTTP_CODE_OK) { https.writeToStream(doc); + https.end(); return true; } else { printE("Communication error: "); printE(https.errorToString(status)); printI(url); printD(https.getString()); + https.end(); return false; } } else { @@ -225,6 +227,7 @@ double EntsoeApi::getCurrencyMultiplier(const char* from, const char* to) { printD(https.getString()); } lastCurrencyFetch = now; + https.end(); } else { return false; } From 837c3cf802cf5be2e2b027a1e9bae949f46ff7a8 Mon Sep 17 00:00:00 2001 From: Gunnar Skjold Date: Mon, 11 Jan 2021 20:21:36 +0100 Subject: [PATCH 06/22] Taking timezone into account when calculating midnight --- src/entsoe/EntsoeApi.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/entsoe/EntsoeApi.cpp b/src/entsoe/EntsoeApi.cpp index 08058376..64ed839b 100644 --- a/src/entsoe/EntsoeApi.cpp +++ b/src/entsoe/EntsoeApi.cpp @@ -85,7 +85,7 @@ bool EntsoeApi::loop() { uint64_t now = millis64(); if(midnightMillis == 0) { - time_t epoch = time(nullptr); + time_t epoch = tz->toLocal(time(nullptr)); tmElements_t tm; breakTime(epoch, tm); @@ -93,8 +93,8 @@ bool EntsoeApi::loop() { 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; + printD("Setting midnight millis " + String((uint32_t) midnightMillis)); } } else if(now > midnightMillis) { printD("Rotating price objects"); @@ -130,7 +130,7 @@ bool EntsoeApi::loop() { } if(tomorrow == NULL - && midnightMillis - now < 43200000 + && midnightMillis - now < 39600000 && (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 3600000) ) { time_t e1 = time(nullptr); From 037bac24de1b30930167017795d1097897597c0d Mon Sep 17 00:00:00 2001 From: Gunnar Skjold Date: Thu, 14 Jan 2021 16:19:00 +0100 Subject: [PATCH 07/22] Changes in user interface --- src/AmsData.cpp | 26 +++++ src/AmsData.h | 3 +- src/AmsToMqttBridge.ino | 135 ++++++++++++++---------- src/entsoe/EntsoeApi.cpp | 7 +- src/entsoe/EntsoeApi.h | 1 + src/web/AmsWebServer.cpp | 23 ++++- web/application.js | 91 ++++++++++++---- web/gaugemeter.js | 12 +++ web/head.html | 2 +- web/index.html | 217 ++++++++++++++++++++++----------------- web/price.html | 1 + 11 files changed, 339 insertions(+), 179 deletions(-) diff --git a/src/AmsData.cpp b/src/AmsData.cpp index 9295af95..1082462e 100644 --- a/src/AmsData.cpp +++ b/src/AmsData.cpp @@ -287,6 +287,30 @@ void AmsData::extractFromOmnipower(HanReader& hanReader, int listSize) { } void AmsData::apply(AmsData& other) { + if(other.getListType() < 3) { + unsigned long ms = this->lastUpdateMillis > other.getLastUpdateMillis() ? 0 : other.getLastUpdateMillis() - this->lastUpdateMillis; + + if(ms > 0) { + if(other.getActiveImportPower() > 0) + activeImportCounter += (((double) ms) * other.getActiveImportPower()) / 3600000000; + counterEstimated = true; + } + + if(other.getListType() > 1) { + unsigned long ms2 = this->lastList2UpdateMillis > other.getLastUpdateMillis() ? 0 : other.getLastUpdateMillis() - this->lastList2UpdateMillis; + if(ms2 > 0) { + // Not sure why, but I cannot make these numbers correct. It seems to be double of what it should, so dividing it by two... + if(other.getActiveExportPower() > 0) + activeExportCounter += (((double) ms2/2) * other.getActiveExportPower()) / 3600000000; + if(other.getReactiveImportPower() > 0) + reactiveImportCounter += (((double) ms2/2) * other.getReactiveImportPower()) / 3600000000; + if(other.getReactiveExportPower() > 0) + reactiveExportCounter += (((double) ms2/2) * other.getReactiveExportPower()) / 3600000000; + counterEstimated = true; + } + } + } + this->lastUpdateMillis = other.getLastUpdateMillis(); this->packageTimestamp = other.getPackageTimestamp(); this->listType = max(this->listType, other.getListType()); @@ -297,6 +321,7 @@ void AmsData::apply(AmsData& other) { this->activeExportCounter = other.getActiveExportCounter(); this->reactiveImportCounter = other.getReactiveImportCounter(); this->reactiveExportCounter = other.getReactiveExportCounter(); + this->counterEstimated = false; case 2: this->listId = other.getListId(); this->meterId = other.getMeterId(); @@ -311,6 +336,7 @@ void AmsData::apply(AmsData& other) { this->l2voltage = other.getL2Voltage(); this->l3voltage = other.getL3Voltage(); this->threePhase = other.isThreePhase(); + this->lastList2UpdateMillis = other.getLastUpdateMillis(); case 1: this->activeImportPower = other.getActiveImportPower(); } diff --git a/src/AmsData.h b/src/AmsData.h index dbb0930b..a5d3f680 100644 --- a/src/AmsData.h +++ b/src/AmsData.h @@ -51,6 +51,7 @@ public: private: unsigned long lastUpdateMillis = 0; + unsigned long lastList2UpdateMillis = 0; int listType = 0; unsigned long packageTimestamp = 0; String listId, meterId, meterType; @@ -58,7 +59,7 @@ private: int activeImportPower = 0, reactiveImportPower = 0, activeExportPower = 0, reactiveExportPower = 0; double l1voltage = 0, l2voltage = 0, l3voltage = 0, l1current = 0, l2current = 0, l3current = 0; double activeImportCounter = 0, reactiveImportCounter = 0, activeExportCounter = 0, reactiveExportCounter = 0; - bool threePhase = false; + bool threePhase = false, counterEstimated = false; void extractFromKaifa(HanReader& hanReader, int listSize); void extractFromAidon(HanReader& hanReader, int listSize, bool substituteMissing); diff --git a/src/AmsToMqttBridge.ino b/src/AmsToMqttBridge.ino index c8108e43..0d4c198a 100644 --- a/src/AmsToMqttBridge.ino +++ b/src/AmsToMqttBridge.ino @@ -459,11 +459,11 @@ void loop() { delay(1); readHanPort(); if(WiFi.status() == WL_CONNECTED) { - ws.loop(); - if(eapi.loop()) { - sendPricesToMqtt(); - } + //if(eapi.loop()) { + // sendPricesToMqtt(); + //} } + ws.loop(); delay(1); // Needed for auto modem sleep } @@ -611,62 +611,77 @@ void mqttMessageReceived(String &topic, String &payload) } void sendPricesToMqtt() { + if(strlen(config.getMqttHost()) == 0 || strlen(config.getMqttPublishTopic()) == 0) + return; + if(strcmp(config.getEntsoeApiToken(), "") != 0) + return; + + time_t now = time(nullptr); + 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; + double values[24] = {0}; + for(int i = 0; i < 24; i++) { + double val = eapi.getValueForHour(now, i); + values[i] = val; - if(val1 == ENTSOE_NO_VALUE) break; + if(val == ENTSOE_NO_VALUE) break; - if(val1 < min) min = val1; - if(val1 > max) max = val1; + if(val < min) min = val; + if(val > max) max = val; - if(i >= 24) continue; // Only estimate 1hr, 3hr and 6hr cheapest interval for next 24 hrs - - if(min1hrIdx == -1 || min1hr > val1) { - min1hr = val1; + if(min1hrIdx == -1 || min1hr > val) { + min1hr = val; 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; + if(i >= 2) { + double val1 = values[i-2]; + double val2 = values[i-1]; + double val3 = val; + if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE) continue; + double val3hr = val1+val2+val3; + if(min3hrIdx == -1 || min3hr > val3hr) { + min3hr = val3hr; + min3hrIdx = i-2; + } } - 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; + + if(i >= 5) { + double val1 = values[i-5]; + double val2 = values[i-4]; + double val3 = values[i-3]; + double val4 = values[i-2]; + double val5 = values[i-1]; + double val6 = val; + if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE || 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-5; + } } + } char ts1hr[21]; if(min1hrIdx != -1) { tmElements_t tm; - breakTime(time(nullptr) + (SECS_PER_HOUR * min1hrIdx), tm); + breakTime(now + (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); + breakTime(now + (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); + breakTime(now + (SECS_PER_HOUR * min6hrIdx), tm); sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour); } @@ -678,33 +693,37 @@ void sendPricesToMqtt() { 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); - } + for(int i = 0; i < 24; 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(ts3hr); + } + if(min6hrIdx != -1) { + jp["cheapest6hr"] = String(ts6hr); + } + + String msg; + serializeJson(json, msg); + mqtt.publish(config.getMqttPublishTopic(), msg.c_str()); 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++) { + { + for(int i = 0; i < 24; i++) { double val = values[i]; if(val == ENTSOE_NO_VALUE) { mqtt.publish(String(config.getMqttPublishTopic()) + "/price/" + String(i), ""); @@ -712,6 +731,8 @@ void sendPricesToMqtt() { } else { mqtt.publish(String(config.getMqttPublishTopic()) + "/price/" + String(i), String(val, 4)); } + mqtt.loop(); + delay(10); } if(min != INT16_MAX) { mqtt.publish(String(config.getMqttPublishTopic()) + "/price/min", String(min, 4)); @@ -719,6 +740,7 @@ void sendPricesToMqtt() { 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)); } @@ -910,7 +932,6 @@ 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 diff --git a/src/entsoe/EntsoeApi.cpp b/src/entsoe/EntsoeApi.cpp index 64ed839b..ef147a58 100644 --- a/src/entsoe/EntsoeApi.cpp +++ b/src/entsoe/EntsoeApi.cpp @@ -40,10 +40,13 @@ void EntsoeApi::setMultiplier(double multiplier) { char* EntsoeApi::getCurrency() { return currency; } - double EntsoeApi::getValueForHour(int hour) { - tmElements_t tm; time_t cur = time(nullptr); + return getValueForHour(cur, hour); +} + +double EntsoeApi::getValueForHour(time_t cur, int hour) { + tmElements_t tm; if(tz != NULL) cur = tz->toLocal(cur); breakTime(cur, tm); diff --git a/src/entsoe/EntsoeApi.h b/src/entsoe/EntsoeApi.h index 4e790ced..cd1f377d 100644 --- a/src/entsoe/EntsoeApi.h +++ b/src/entsoe/EntsoeApi.h @@ -15,6 +15,7 @@ public: bool loop(); double getValueForHour(int hour); + double getValueForHour(time_t now, int hour); char* getCurrency(); void setToken(const char* token); diff --git a/src/web/AmsWebServer.cpp b/src/web/AmsWebServer.cpp index fd8afbd1..207a21f8 100644 --- a/src/web/AmsWebServer.cpp +++ b/src/web/AmsWebServer.cpp @@ -295,19 +295,18 @@ void AmsWebServer::indexHtml() { html.replace("${data.P}", String(data.getActiveImportPower())); html.replace("${data.PO}", String(data.getActiveExportPower())); html.replace("${display.export}", config->getProductionCapacity() > 0 ? "" : "none"); - html.replace("${text.import}", config->getProductionCapacity() > 0 ? "Import" : "Consumption"); + html.replace("${display.nonexport}", config->getProductionCapacity() > 0 ? "none" : ""); + html.replace("${text.import}", config->getProductionCapacity() > 0 ? "Import" : "Use"); + html.replace("${display.3p}", data.isThreePhase() ? "" : "none"); html.replace("${data.U1}", u1 > 0 ? String(u1, 1) : ""); html.replace("${data.I1}", u1 > 0 ? String(i1, 1) : ""); - html.replace("${display.P1}", u1 > 0 ? "" : "none"); html.replace("${data.U2}", u2 > 0 ? String(u2, 1) : ""); html.replace("${data.I2}", u2 > 0 ? String(i2, 1) : ""); - html.replace("${display.P2}", u2 > 0 ? "" : "none"); html.replace("${data.U3}", u3 > 0 ? String(u3, 1) : ""); html.replace("${data.I3}", u3 > 0 ? String(i3, 1) : ""); - html.replace("${display.P3}", u3 > 0 ? "" : "none"); html.replace("${data.tPI}", tpi > 0 ? String(tpi, 1) : ""); html.replace("${data.tPO}", tpi > 0 ? String(tpo, 1) : ""); @@ -327,6 +326,8 @@ void AmsWebServer::indexHtml() { html.replace("${wifi.channel}", WiFi.channel() > 0 ? String(WiFi.channel()) : ""); html.replace("${wifi.ssid}", !WiFi.SSID().isEmpty() ? String(WiFi.SSID()) : ""); + html.replace("${currentSeconds}", String((uint32_t)(millis64()/1000), 10)); + server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN); server.send_P(200, "text/html", HEAD_HTML); server.sendContent(html); @@ -628,6 +629,9 @@ void AmsWebServer::dataJson() { double tqi = data.getReactiveImportCounter(); double tqo = data.getReactiveExportCounter(); + double volt = u1; + double amp = i1; + if(u1 > 0) { json["data"]["U1"] = u1; json["data"]["I1"] = i1; @@ -635,10 +639,13 @@ void AmsWebServer::dataJson() { if(u2 > 0) { json["data"]["U2"] = u2; json["data"]["I2"] = i2; + if(i2 > amp) amp = i2; } if(u3 > 0) { json["data"]["U3"] = u3; json["data"]["I3"] = i3; + volt = (u1+u2+u3)/3; + if(i3 > amp) amp = i3; } if(tpi > 0) { @@ -650,6 +657,14 @@ void AmsWebServer::dataJson() { json["p_pct"] = min(data.getActiveImportPower()*100/maxPwr, 100); + json["v"] = volt; + json["v_pct"] = (max((int)volt-207, 1)*100/46); + + int maxAmp = config->getMainFuse() == 0 ? 32 : config->getMainFuse(); + + json["a"] = amp; + json["a_pct"] = amp * 100 / maxAmp; + if(config->getProductionCapacity() > 0) { int maxPrd = config->getProductionCapacity() * 1000; json["po_pct"] = min(data.getActiveExportPower()*100/maxPrd, 100); diff --git a/web/application.js b/web/application.js index a3732eb4..749f3d40 100644 --- a/web/application.js +++ b/web/application.js @@ -1,5 +1,5 @@ var nextVersion; -var im, em; +var im, em, vm, am; $(function() { im = $("#importMeter"); if(im && im.gaugeMeter) { @@ -18,6 +18,24 @@ $(function() { append: "W" }); } + + vm = $("#voltMeter"); + if(vm && vm.gaugeMeter) { + vm.gaugeMeter({ + percent: 0, + text: "-", + append: "V" + }); + } + + am = $("#ampMeter"); + if(am && am.gaugeMeter) { + am.gaugeMeter({ + percent: 0, + text: "-", + append: "A" + }); + } var meters = $('.SimpleMeter'); @@ -217,10 +235,12 @@ var fetch = function() { timeout: 10000, dataType: 'json', }).done(function(json) { - if(im && em) { + if(im) { $(".SimpleMeter").hide(); im.show(); em.show(); + vm.show(); + am.show(); } for(var id in json) { @@ -237,8 +257,8 @@ var fetch = function() { } if(window.moment) { - $('#currentMillis').html(moment.duration(parseInt(json.uptime_seconds), 'seconds').humanize()); - $('#currentMillis').closest('.row').show(); + $('.currentSeconds').html(moment.duration(parseInt(json.uptime_seconds), 'seconds').humanize()); + $('.currentSeconds').closest('.row').show(); } if(json.status) { @@ -304,16 +324,46 @@ var fetch = function() { }); } + var v = parseFloat(json.v); + if(v > 0) { + var v_pct = parseInt(json.v_pct); + + if(vm && vm.gaugeMeter) { + vm.gaugeMeter({ + percent: v_pct, + text: v.toFixed(1) + }); + } + } + + var a = parseFloat(json.a); + if(a > 0) { + var a_pct = parseInt(json.a_pct); + + if(am && am.gaugeMeter) { + am.gaugeMeter({ + percent: a_pct, + text: a.toFixed(1) + }); + } + } + for(var id in json.data) { var str = json.data[id]; if(isNaN(str)) { - $('#'+id).html(str); + $('.'+id).html(str); } else { var num = parseFloat(str); - $('#'+id).html(num.toFixed(1)); + $('.'+id).html(num.toFixed(1)); $('#'+id+'-row').show(); + $('.'+id+'-row').show(); } } + + var temp = parseInt(json.temp); + if(temp == -127) { + $('.temp').html("N/A"); + } } else { if(im && im.gaugeMeter) { im.gaugeMeter({ @@ -330,26 +380,25 @@ var fetch = function() { append: "W" }); } + + if(vm && vm.gaugeMeter) { + vm.gaugeMeter({ + percent: 0, + text: "-" + }); + } + + if(am && am.gaugeMeter) { + am.gaugeMeter({ + percent: 0, + text: "-" + }); + } } setTimeout(fetch, interval); }).fail(function() { setTimeout(fetch, interval*4); - if(im && im.gaugeMeter) { - im.gaugeMeter({ - percent: 0, - text: "-", - append: "W" - }); - } - - if(em && em.gaugeMeter) { - em.gaugeMeter({ - percent: 0, - text: "-", - append: "W" - }); - } setStatus("mqtt", "secondary"); setStatus("wifi", "secondary"); setStatus("han", "secondary"); diff --git a/web/gaugemeter.js b/web/gaugemeter.js index 0e9b54fb..6978a738 100644 --- a/web/gaugemeter.js +++ b/web/gaugemeter.js @@ -55,6 +55,18 @@ "LightGreen-DarkGreen" === option.theme && (e > 0 && (t = "#3afc00"), e > 10 && (t = "#39f900"), e > 20 && (t = "#38f600"), e > 30 && (t = "#38f100"), e > 40 && (t = "#37ec00"), e > 50 && (t = "#36e700"), e > 60 && (t = "#34e200"), e > 70 && (t = "#34df00"), e > 80 && (t = "#33db00"), e > 90 && (t = "#32d900")), "DarkGold-LightGold" === option.theme && (e > 0 && (t = "#ffb800"), e > 10 && (t = "#ffba00"), e > 20 && (t = "#ffbd00"), e > 30 && (t = "#ffc200"), e > 40 && (t = "#ffc600"), e > 50 && (t = "#ffcb00"), e > 60 && (t = "#ffcf00"), e > 70 && (t = "#ffd400"), e > 80 && (t = "#ffd600"), e > 90 && (t = "#ffd900")), "LightGold-DarkGold" === option.theme && (e > 0 && (t = "#ffd900"), e > 10 && (t = "#ffd600"), e > 20 && (t = "#ffd400"), e > 30 && (t = "#ffcf00"), e > 40 && (t = "#ffcb00"), e > 50 && (t = "#ffc600"), e > 60 && (t = "#ffc200"), e > 70 && (t = "#ffbd00"), e > 80 && (t = "#ffba00"), e > 90 && (t = "#ffb800")), + "Voltage" === option.theme && ( + e > 0 && (t = "#d90000"), + e > 10 && (t = "#f35100"), + e > 20 && (t = "#ffb800"), + e > 30 && (t = "#a6d900"), + e > 40 && (t = "#32d900"), + e > 50 && (t = "#32d900"), + e > 60 && (t = "#a6d900"), + e > 70 && (t = "#ffb800"), + e > 80 && (t = "#f35100"), + e > 90 && (t = "#d90000") + ), "White" === option.theme && (t = "#fff"), "Black" === option.theme && (t = "#000"), t; diff --git a/web/head.html b/web/head.html index 2b2fd101..86a54165 100644 --- a/web/head.html +++ b/web/head.html @@ -49,7 +49,7 @@
-