From b592532c1d71a030fabc2df912074165e244117c Mon Sep 17 00:00:00 2001 From: Gunnar Skjold Date: Thu, 5 Jun 2025 15:28:15 +0200 Subject: [PATCH] WIP more changes for 15min prices --- .../include/EnergyAccounting.h | 39 ++- lib/EnergyAccounting/src/EnergyAccounting.cpp | 247 +++++++++--------- .../src/HomeAssistantMqttHandler.cpp | 4 +- lib/JsonMqttHandler/src/JsonMqttHandler.cpp | 2 +- lib/PriceService/include/PriceService.h | 25 +- lib/PriceService/include/PricesContainer.h | 21 +- lib/PriceService/src/EntsoeA44Parser.cpp | 8 +- lib/PriceService/src/PriceService.cpp | 19 +- lib/PriceService/src/PricesContainer.cpp | 67 +++++ lib/RawMqttHandler/src/RawMqttHandler.cpp | 2 +- lib/SvelteUi/include/AmsWebServer.h | 5 +- lib/SvelteUi/src/AmsWebServer.cpp | 67 ++++- src/AmsToMqttBridge.cpp | 2 +- 13 files changed, 323 insertions(+), 185 deletions(-) create mode 100644 lib/PriceService/src/PricesContainer.cpp diff --git a/lib/EnergyAccounting/include/EnergyAccounting.h b/lib/EnergyAccounting/include/EnergyAccounting.h index 02aec2fd..60f57f71 100644 --- a/lib/EnergyAccounting/include/EnergyAccounting.h +++ b/lib/EnergyAccounting/include/EnergyAccounting.h @@ -18,6 +18,24 @@ struct EnergyAccountingPeak { }; struct EnergyAccountingData { + uint8_t version; + uint8_t month; + int32_t costToday; + int32_t costYesterday; + int32_t costThisMonth; + int32_t costLastMonth; + int32_t incomeToday; + int32_t incomeYesterday; + int32_t incomeThisMonth; + int32_t incomeLastMonth; + uint32_t lastMonthImport; + uint32_t lastMonthExport; + uint8_t lastMonthAccuracy; + EnergyAccountingPeak peaks[5]; + time_t lastUpdated; +}; + +struct EnergyAccountingData6 { uint8_t version; uint8_t month; int32_t costYesterday; @@ -44,24 +62,6 @@ struct EnergyAccountingData5 { EnergyAccountingPeak peaks[5]; }; -struct EnergyAccountingData4 { - uint8_t version; - uint8_t month; - uint16_t costYesterday; - uint16_t costThisMonth; - uint16_t costLastMonth; - EnergyAccountingPeak peaks[5]; -}; - -struct EnergyAccountingData2 { - uint8_t version; - uint8_t month; - uint16_t maxHour; - uint16_t costYesterday; - uint16_t costThisMonth; - uint16_t costLastMonth; -}; - struct EnergyAccountingRealtimeData { uint8_t magic; uint8_t currentHour; @@ -124,7 +124,6 @@ public: void setData(EnergyAccountingData&); void setCurrency(String currency); - float getPriceForHour(uint8_t d, uint8_t h); private: #if defined(AMS_REMOTE_DEBUG) @@ -137,7 +136,7 @@ private: PriceService *ps = NULL; EnergyAccountingConfig *config = NULL; Timezone *tz = NULL; - EnergyAccountingData data = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + EnergyAccountingData data = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; EnergyAccountingRealtimeData* realtimeData = NULL; String currency = ""; diff --git a/lib/EnergyAccounting/src/EnergyAccounting.cpp b/lib/EnergyAccounting/src/EnergyAccounting.cpp index 54fd83db..c10563cc 100644 --- a/lib/EnergyAccounting/src/EnergyAccounting.cpp +++ b/lib/EnergyAccounting/src/EnergyAccounting.cpp @@ -30,7 +30,7 @@ EnergyAccounting::EnergyAccounting(Stream* Stream, EnergyAccountingRealtimeData* rtd->lastImportUpdateMillis = 0; rtd->lastExportUpdateMillis = 0; } - this->realtimeData = rtd; + realtimeData = rtd; } void EnergyAccounting::setup(AmsDataStorage *ds, EnergyAccountingConfig *config) { @@ -67,31 +67,31 @@ bool EnergyAccounting::update(AmsData* amsData) { breakTime(tz->toLocal(now), local); if(!init) { - this->realtimeData->lastImportUpdateMillis = 0; - this->realtimeData->lastExportUpdateMillis = 0; - this->realtimeData->currentHour = local.Hour; - this->realtimeData->currentDay = local.Day; + realtimeData->lastImportUpdateMillis = 0; + realtimeData->lastExportUpdateMillis = 0; + realtimeData->currentHour = local.Hour; + realtimeData->currentDay = local.Day; if(!load()) { - data = { 6, local.Month, - 0, 0, 0, // Cost - 0, 0, 0, // Income + data = { 7, local.Month, + 0, 0, 0, 0, // Cost + 0, 0, 0, 0, // Income 0, 0, 0, // Last month import, export and accuracy 0, 0, // Peak 1 0, 0, // Peak 2 0, 0, // Peak 3 0, 0, // Peak 4 - 0, 0 // Peak 5 + 0, 0, // Peak 5 + 0 // Last updated }; } init = true; } - float importPrice = getPriceForHour(PRICE_DIRECTION_IMPORT, 0); - if(!initPrice && importPrice != PRICE_NO_VALUE) { + if(!initPrice && ps != NULL && ps->hasPrice()) { calcDayCost(); } - if(local.Hour != this->realtimeData->currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) { + if(local.Hour != realtimeData->currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) { tmElements_t oneHrAgo, oneHrAgoLocal; breakTime(now-3600, oneHrAgo); uint16_t val = round(ds->getHourImport(oneHrAgo.Hour) / 10.0); @@ -99,27 +99,24 @@ bool EnergyAccounting::update(AmsData* amsData) { breakTime(tz->toLocal(now-3600), oneHrAgoLocal); ret |= updateMax(val, oneHrAgoLocal.Day); - this->realtimeData->currentHour = local.Hour; // Need to be defined here so that day cost is correctly calculated - if(local.Hour > 0) { - calcDayCost(); - } + realtimeData->currentHour = local.Hour; // Need to be defined here so that day cost is correctly calculated - this->realtimeData->use = 0; - this->realtimeData->produce = 0; - this->realtimeData->costHour = 0; - this->realtimeData->incomeHour = 0; + realtimeData->use = 0; + realtimeData->produce = 0; + realtimeData->costHour = 0; + realtimeData->incomeHour = 0; - uint8_t prevDay = this->realtimeData->currentDay; - if(local.Day != this->realtimeData->currentDay) { - data.costYesterday = this->realtimeData->costDay * 100; - data.costThisMonth += this->realtimeData->costDay * 100; - this->realtimeData->costDay = 0; + uint8_t prevDay = realtimeData->currentDay; + if(local.Day != realtimeData->currentDay) { + data.costYesterday = realtimeData->costDay * 100; + data.costThisMonth += realtimeData->costDay * 100; + realtimeData->costDay = 0; - data.incomeYesterday = this->realtimeData->incomeDay * 100; - data.incomeThisMonth += this->realtimeData->incomeDay * 100; - this->realtimeData->incomeDay = 0; + data.incomeYesterday = realtimeData->incomeDay * 100; + data.incomeThisMonth += realtimeData->incomeDay * 100; + realtimeData->incomeDay = 0; - this->realtimeData->currentDay = local.Day; + realtimeData->currentDay = local.Day; ret = true; } @@ -149,42 +146,49 @@ bool EnergyAccounting::update(AmsData* amsData) { data.lastMonthAccuracy = accuracy; data.month = local.Month; - this->realtimeData->currentThresholdIdx = 0; + realtimeData->currentThresholdIdx = 0; ret = true; } + + if(ret) { + data.costToday = realtimeData->costDay * 100; + data.incomeToday = realtimeData->incomeDay * 100; + data.lastUpdated = now; + } } - if(this->realtimeData->lastImportUpdateMillis < amsData->getLastUpdateMillis()) { - unsigned long ms = amsData->getLastUpdateMillis() - this->realtimeData->lastImportUpdateMillis; + if(realtimeData->lastImportUpdateMillis < amsData->getLastUpdateMillis()) { + unsigned long ms = amsData->getLastUpdateMillis() - realtimeData->lastImportUpdateMillis; float kwhi = (amsData->getActiveImportPower() * (((float) ms) / 3600000.0)) / 1000.0; if(kwhi > 0) { - this->realtimeData->use += kwhi; + realtimeData->use += kwhi; + float importPrice = ps == NULL ? PRICE_NO_VALUE : ps->getPrice(PRICE_DIRECTION_IMPORT); if(importPrice != PRICE_NO_VALUE) { float cost = importPrice * kwhi; - this->realtimeData->costHour += cost; - this->realtimeData->costDay += cost; + realtimeData->costHour += cost; + realtimeData->costDay += cost; } } - this->realtimeData->lastImportUpdateMillis = amsData->getLastUpdateMillis(); + realtimeData->lastImportUpdateMillis = amsData->getLastUpdateMillis(); } - if(amsData->getListType() > 1 && this->realtimeData->lastExportUpdateMillis < amsData->getLastUpdateMillis()) { - unsigned long ms = amsData->getLastUpdateMillis() - this->realtimeData->lastExportUpdateMillis; + if(amsData->getListType() > 1 && realtimeData->lastExportUpdateMillis < amsData->getLastUpdateMillis()) { + unsigned long ms = amsData->getLastUpdateMillis() - realtimeData->lastExportUpdateMillis; float kwhe = (amsData->getActiveExportPower() * (((float) ms) / 3600000.0)) / 1000.0; if(kwhe > 0) { - this->realtimeData->produce += kwhe; - float exportPrice = getPriceForHour(PRICE_DIRECTION_EXPORT, 0); + realtimeData->produce += kwhe; + float exportPrice = ps == NULL ? PRICE_NO_VALUE : ps->getPrice(PRICE_DIRECTION_EXPORT); if(exportPrice != PRICE_NO_VALUE) { float income = exportPrice * kwhe; - this->realtimeData->incomeHour += income; - this->realtimeData->incomeDay += income; + realtimeData->incomeHour += income; + realtimeData->incomeDay += income; } } - this->realtimeData->lastExportUpdateMillis = amsData->getLastUpdateMillis(); + realtimeData->lastExportUpdateMillis = amsData->getLastUpdateMillis(); } if(config != NULL) { - while(getMonthMax() > config->thresholds[this->realtimeData->currentThresholdIdx] && this->realtimeData->currentThresholdIdx < 10) this->realtimeData->currentThresholdIdx++; + while(getMonthMax() > config->thresholds[realtimeData->currentThresholdIdx] && realtimeData->currentThresholdIdx < 10) realtimeData->currentThresholdIdx++; } return ret; @@ -192,28 +196,36 @@ bool EnergyAccounting::update(AmsData* amsData) { void EnergyAccounting::calcDayCost() { time_t now = time(nullptr); - tmElements_t local, utc; + tmElements_t local, utc, lastUpdateUtc; if(tz == NULL) return; breakTime(tz->toLocal(now), local); + if(ps == NULL) return; - if(getPriceForHour(PRICE_DIRECTION_IMPORT, 0) != PRICE_NO_VALUE) { - if(initPrice) { - this->realtimeData->costDay = 0; - this->realtimeData->incomeDay = 0; + if(ps->hasPrice()) { + breakTime(data.lastUpdated, lastUpdateUtc); + uint8_t calcFromHour = 0; + if(lastUpdateUtc.Day != local.Day || lastUpdateUtc.Month != local.Month || lastUpdateUtc.Year != local.Year) { + realtimeData->costDay = 0; + realtimeData->incomeDay = 0; + calcFromHour = 0; + } else { + realtimeData->costDay = data.costToday / 100.0; + realtimeData->incomeDay = data.incomeToday / 100.0; + calcFromHour = lastUpdateUtc.Hour; } - for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) { + for(uint8_t i = calcFromHour; i < realtimeData->currentHour; i++) { breakTime(now - ((local.Hour - i) * 3600), utc); - float priceIn = getPriceForHour(PRICE_DIRECTION_IMPORT, i - local.Hour); + float priceIn = ps->getPriceForHour(PRICE_DIRECTION_IMPORT, i - local.Hour); if(priceIn != PRICE_NO_VALUE) { int16_t wh = ds->getHourImport(utc.Hour); - this->realtimeData->costDay += priceIn * (wh / 1000.0); + realtimeData->costDay += priceIn * (wh / 1000.0); } - float priceOut = getPriceForHour(PRICE_DIRECTION_EXPORT, i - local.Hour); + float priceOut = ps->getPriceForHour(PRICE_DIRECTION_EXPORT, i - local.Hour); if(priceOut != PRICE_NO_VALUE) { int16_t wh = ds->getHourExport(utc.Hour); - this->realtimeData->incomeDay += priceOut * (wh / 1000.0); + realtimeData->incomeDay += priceOut * (wh / 1000.0); } } initPrice = true; @@ -221,7 +233,7 @@ void EnergyAccounting::calcDayCost() { } float EnergyAccounting::getUseThisHour() { - return this->realtimeData->use; + return realtimeData->use; } float EnergyAccounting::getUseToday() { @@ -231,7 +243,7 @@ float EnergyAccounting::getUseToday() { if(now < FirmwareVersion::BuildEpoch) return 0.0; tmElements_t utc, local; breakTime(tz->toLocal(now), local); - for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) { + for(uint8_t i = 0; i < realtimeData->currentHour; i++) { breakTime(now - ((local.Hour - i) * 3600), utc); ret += ds->getHourImport(utc.Hour) / 1000.0; } @@ -242,7 +254,7 @@ float EnergyAccounting::getUseThisMonth() { time_t now = time(nullptr); if(now < FirmwareVersion::BuildEpoch) return 0.0; float ret = 0; - for(uint8_t i = 1; i < this->realtimeData->currentDay; i++) { + for(uint8_t i = 1; i < realtimeData->currentDay; i++) { ret += ds->getDayImport(i) / 1000.0; } return ret + getUseToday(); @@ -253,7 +265,7 @@ float EnergyAccounting::getUseLastMonth() { } float EnergyAccounting::getProducedThisHour() { - return this->realtimeData->produce; + return realtimeData->produce; } float EnergyAccounting::getProducedToday() { @@ -263,7 +275,7 @@ float EnergyAccounting::getProducedToday() { if(now < FirmwareVersion::BuildEpoch) return 0.0; tmElements_t utc, local; breakTime(tz->toLocal(now), local); - for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) { + for(uint8_t i = 0; i < realtimeData->currentHour; i++) { breakTime(now - ((local.Hour - i) * 3600), utc); ret += ds->getHourExport(utc.Hour) / 1000.0; } @@ -274,7 +286,7 @@ float EnergyAccounting::getProducedThisMonth() { time_t now = time(nullptr); if(now < FirmwareVersion::BuildEpoch) return 0.0; float ret = 0; - for(uint8_t i = 1; i < this->realtimeData->currentDay; i++) { + for(uint8_t i = 1; i < realtimeData->currentDay; i++) { ret += ds->getDayExport(i) / 1000.0; } return ret + getProducedToday(); @@ -285,11 +297,11 @@ float EnergyAccounting::getProducedLastMonth() { } float EnergyAccounting::getCostThisHour() { - return this->realtimeData->costHour; + return realtimeData->costHour; } float EnergyAccounting::getCostToday() { - return this->realtimeData->costDay; + return realtimeData->costDay; } float EnergyAccounting::getCostYesterday() { @@ -305,11 +317,11 @@ float EnergyAccounting::getCostLastMonth() { } float EnergyAccounting::getIncomeThisHour() { - return this->realtimeData->incomeHour; + return realtimeData->incomeHour; } float EnergyAccounting::getIncomeToday() { - return this->realtimeData->incomeDay; + return realtimeData->incomeDay; } float EnergyAccounting::getIncomeYesterday() { @@ -327,7 +339,7 @@ float EnergyAccounting::getIncomeLastMonth() { uint8_t EnergyAccounting::getCurrentThreshold() { if(config == NULL) return 0; - return config->thresholds[this->realtimeData->currentThresholdIdx]; + return config->thresholds[realtimeData->currentThresholdIdx]; } float EnergyAccounting::getMonthMax() { @@ -407,85 +419,65 @@ bool EnergyAccounting::load() { char buf[file.size()]; file.readBytes(buf, file.size()); - if(buf[0] == 6) { + if(buf[0] == 7) { EnergyAccountingData* data = (EnergyAccountingData*) buf; memcpy(&this->data, data, sizeof(this->data)); ret = true; - } else if(buf[0] == 5) { - EnergyAccountingData5* data = (EnergyAccountingData5*) buf; - this->data = { 6, data->month, - ((uint32_t) data->costYesterday) * 10, - ((uint32_t) data->costThisMonth) * 100, - ((uint32_t) data->costLastMonth) * 100, - ((uint32_t) data->incomeYesterday) * 10, - ((uint32_t) data->incomeThisMonth) * 100, - ((uint32_t) data->incomeLastMonth) * 100, - 0,0,0, // Last month import, export and accuracy - data->peaks[0].day, data->peaks[0].value, - data->peaks[1].day, data->peaks[1].value, - data->peaks[2].day, data->peaks[2].value, - data->peaks[3].day, data->peaks[3].value, - data->peaks[4].day, data->peaks[4].value - }; - ret = true; - } else if(buf[0] == 4) { - EnergyAccountingData4* data = (EnergyAccountingData4*) buf; - this->data = { 5, data->month, - ((uint32_t) data->costYesterday) * 10, - ((uint32_t) data->costThisMonth) * 100, - ((uint32_t) data->costLastMonth) * 100, - 0,0,0, // Income from production - 0,0,0, // Last month import, export and accuracy - data->peaks[0].day, data->peaks[0].value, - data->peaks[1].day, data->peaks[1].value, - data->peaks[2].day, data->peaks[2].value, - data->peaks[3].day, data->peaks[3].value, - data->peaks[4].day, data->peaks[4].value - }; - ret = true; - } else if(buf[0] == 3) { - EnergyAccountingData* data = (EnergyAccountingData*) buf; - this->data = { 5, data->month, - data->costYesterday * 10, + } else if(buf[0] == 6) { + EnergyAccountingData6* data = (EnergyAccountingData6*) buf; + this->data = { 7, data->month, + 0, // Cost today + data->costYesterday, data->costThisMonth, data->costLastMonth, - 0,0,0, // Income from production + 0, // Income today + data->incomeYesterday, + data->incomeThisMonth, + data->incomeLastMonth, + data->lastMonthImport, + data->lastMonthExport, + data->lastMonthAccuracy, + data->peaks[0].day, data->peaks[0].value, + data->peaks[1].day, data->peaks[1].value, + data->peaks[2].day, data->peaks[2].value, + data->peaks[3].day, data->peaks[3].value, + data->peaks[4].day, data->peaks[4].value, + 0 // Last updated + }; + ret = true; + } else if(buf[0] == 5) { + EnergyAccountingData5* data = (EnergyAccountingData5*) buf; + this->data = { 7, data->month, + 0, // Cost today + ((int32_t) data->costYesterday) * 10, + ((int32_t) data->costThisMonth) * 100, + ((int32_t) data->costLastMonth) * 100, + 0, // Income today + ((int32_t) data->incomeYesterday) * 10, + ((int32_t) data->incomeThisMonth) * 100, + ((int32_t) data->incomeLastMonth) * 100, 0,0,0, // Last month import, export and accuracy data->peaks[0].day, data->peaks[0].value, data->peaks[1].day, data->peaks[1].value, data->peaks[2].day, data->peaks[2].value, data->peaks[3].day, data->peaks[3].value, - data->peaks[4].day, data->peaks[4].value + data->peaks[4].day, data->peaks[4].value, + 0 // Last updated }; ret = true; } else { - data = { 5, 0, - 0, 0, 0, // Cost - 0,0,0, // Income from production + data = { 7, 0, + 0,0,0,0, // Cost + 0,0,0,0, // Income from production 0,0,0, // Last month import, export and accuracy 0, 0, // Peak 1 0, 0, // Peak 2 0, 0, // Peak 3 0, 0, // Peak 4 - 0, 0 // Peak 5 + 0, 0, // Peak 5 + 0 // Last updated }; - if(buf[0] == 2) { - EnergyAccountingData2* data = (EnergyAccountingData2*) buf; - this->data.month = data->month; - this->data.costYesterday = data->costYesterday * 10; - this->data.costThisMonth = data->costThisMonth; - this->data.costLastMonth = data->costLastMonth; - uint8_t b = 0; - for(uint8_t i = sizeof(this->data); i < file.size(); i+=2) { - this->data.peaks[b].day = b; - memcpy(&this->data.peaks[b].value, buf+i, 2); - b++; - if(b >= config->hours || b >= 5) break; - } - ret = true; - } else { - ret = false; - } + ret = false; } file.close(); @@ -550,8 +542,3 @@ bool EnergyAccounting::updateMax(uint16_t val, uint8_t day) { void EnergyAccounting::setCurrency(String currency) { this->currency = currency; } - -float EnergyAccounting::getPriceForHour(uint8_t d, uint8_t h) { - if(ps == NULL) return PRICE_NO_VALUE; - return ps->getValueForHour(d, h); -} \ No newline at end of file diff --git a/lib/HomeAssistantMqttHandler/src/HomeAssistantMqttHandler.cpp b/lib/HomeAssistantMqttHandler/src/HomeAssistantMqttHandler.cpp index dbcaf1c6..165e4a9a 100644 --- a/lib/HomeAssistantMqttHandler/src/HomeAssistantMqttHandler.cpp +++ b/lib/HomeAssistantMqttHandler/src/HomeAssistantMqttHandler.cpp @@ -360,7 +360,7 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) { if(pubTopic.isEmpty() || !mqtt.connected()) return false; - if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE) + if(!ps->hasPrice()) return false; publishPriceSensors(ps); @@ -712,7 +712,7 @@ void HomeAssistantMqttHandler::publishPriceSensors(PriceService* ps) { prInit[i] = true; } - float exportPrice = ps->getValueForHour(PRICE_DIRECTION_EXPORT, 0); + float exportPrice = ps->getPrice(PRICE_DIRECTION_EXPORT); if(exportPrice != PRICE_NO_VALUE) { char path[20]; snprintf(path, 20, "exportprices['%d']", 0); diff --git a/lib/JsonMqttHandler/src/JsonMqttHandler.cpp b/lib/JsonMqttHandler/src/JsonMqttHandler.cpp index 530eab9d..20168799 100644 --- a/lib/JsonMqttHandler/src/JsonMqttHandler.cpp +++ b/lib/JsonMqttHandler/src/JsonMqttHandler.cpp @@ -274,7 +274,7 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) bool JsonMqttHandler::publishPrices(PriceService* ps) { if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected()) return false; - if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE) + if(!ps->hasPrice()) return false; time_t now = time(nullptr); diff --git a/lib/PriceService/include/PriceService.h b/lib/PriceService/include/PriceService.h index 92aa32d1..3571a002 100644 --- a/lib/PriceService/include/PriceService.h +++ b/lib/PriceService/include/PriceService.h @@ -27,10 +27,6 @@ #define SSL_BUF_SIZE 512 -#define PRICE_DIRECTION_IMPORT 0x01 -#define PRICE_DIRECTION_EXPORT 0x02 -#define PRICE_DIRECTION_BOTH 0x03 - #define PRICE_DAY_MO 0x01 #define PRICE_DAY_TU 0x02 #define PRICE_DAY_WE 0x04 @@ -58,10 +54,11 @@ struct PriceConfig { }; struct AmsPriceV2Header { - char source[4]; char currency[4]; + char measurementUnit[4]; + char source[4]; uint8_t resolutionInMinutes; - uint8_t hours; + bool differentExportPrices; uint8_t numberOfPoints; }; @@ -80,6 +77,22 @@ public: char* getCurrency(); char* getArea(); char* getSource(); + + uint8_t getResolutionInMinutes(); + uint8_t getNumberOfPointsAvailable(); + + bool isExportPricesDifferentFromImport() { + return today->isExportPricesDifferentFromImport(); + } + + bool hasPrice() { return hasPrice(PRICE_DIRECTION_IMPORT); } + bool hasPrice(uint8_t direction); + bool hasPricePoint(uint8_t direction, int8_t point); + + float getPrice(uint8_t direction); + float getPricePoint(uint8_t direction, int8_t point); + float getPriceForHour(uint8_t direction, int8_t hour); // If not 60min interval, average + float getValueForHour(uint8_t direction, int8_t hour); float getValueForHour(uint8_t direction, time_t ts, int8_t hour); diff --git a/lib/PriceService/include/PricesContainer.h b/lib/PriceService/include/PricesContainer.h index b46e56d9..081fab14 100644 --- a/lib/PriceService/include/PricesContainer.h +++ b/lib/PriceService/include/PricesContainer.h @@ -4,35 +4,42 @@ * */ +#include + #ifndef _PRICESCONTAINER_H #define _PRICESCONTAINER_H #define PRICE_NO_VALUE -127 +#define PRICE_DIRECTION_IMPORT 0x01 +#define PRICE_DIRECTION_EXPORT 0x02 +#define PRICE_DIRECTION_BOTH 0x03 class PricesContainer { public: PricesContainer(char* source); - void setup(uint8_t resolutionInMinutes, uint8_t hoursThisDay); + void setup(uint8_t resolutionInMinutes, uint8_t numberOfPoints, bool differentExportPrices); char* getSource(); void setCurrency(char* currency); char* getCurrency(); + bool isExportPricesDifferentFromImport() { + return differentExportPrices; + } + uint8_t getResolutionInMinutes(); - uint8_t getHours(); uint8_t getNumberOfPoints(); - void setPrice(uint8_t point, int32_t value); - void setPrice(uint8_t point, float value); - bool hasPrice(uint8_t point); - float getPrice(uint8_t point); // int32_t / 10_000 + void setPrice(uint8_t point, float value, uint8_t direction); + bool hasPrice(uint8_t point, uint8_t direction); + float getPrice(uint8_t point, uint8_t direction); // int32_t / 10_000 private: char source[4]; char currency[4]; uint8_t resolutionInMinutes; - uint8_t hours; + bool differentExportPrices; uint8_t numberOfPoints; int32_t *points; }; diff --git a/lib/PriceService/src/EntsoeA44Parser.cpp b/lib/PriceService/src/EntsoeA44Parser.cpp index d0e90be7..ce2327c6 100644 --- a/lib/PriceService/src/EntsoeA44Parser.cpp +++ b/lib/PriceService/src/EntsoeA44Parser.cpp @@ -73,7 +73,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) { buf[pos] = '\0'; float val = String(buf).toFloat(); for(uint8_t i = pointNum; i < container->getNumberOfPoints(); i++) { - container->setPrice(i, val * multiplier); + container->setPrice(i, val * multiplier, PRICE_DIRECTION_IMPORT); } docPos = DOCPOS_SEEK; pos = 0; @@ -85,12 +85,12 @@ size_t EntsoeA44Parser::write(uint8_t byte) { buf[pos] = '\0'; // This happens if there are two time series in the XML. We are only interrested in the first one, so we ignore the rest of the document - if(container->hasPrice(0)) return 1; + if(container->hasPrice(0, PRICE_DIRECTION_IMPORT)) return 1; if(strcmp_P(buf, PSTR("PT15M"))) { - container->setup(15, 24); + container->setup(15, 100, false); } else if(strcmp_P(buf, PSTR("PT60M"))) { - container->setup(60, 24); + container->setup(60, 25, false); } docPos = DOCPOS_SEEK; pos = 0; diff --git a/lib/PriceService/src/PriceService.cpp b/lib/PriceService/src/PriceService.cpp index 2e94075d..74424548 100644 --- a/lib/PriceService/src/PriceService.cpp +++ b/lib/PriceService/src/PriceService.cpp @@ -185,18 +185,18 @@ float PriceService::getEnergyPriceForHour(uint8_t direction, time_t ts, int8_t h if(pos >= hoursTomorrow) return PRICE_NO_VALUE; if(tomorrow == NULL) return PRICE_NO_VALUE; - if(!tomorrow->hasPrice(pos)) + if(!tomorrow->hasPrice(pos, direction)) return PRICE_NO_VALUE; - value = tomorrow->getPrice(pos); + value = tomorrow->getPrice(pos, direction); float mult = getCurrencyMultiplier(tomorrow->getCurrency(), config->currency, time(nullptr)); if(mult == 0) return PRICE_NO_VALUE; multiplier *= mult; } else if(pos >= 0) { if(today == NULL) return PRICE_NO_VALUE; - if(!today->hasPrice(pos)) + if(!today->hasPrice(pos, direction)) return PRICE_NO_VALUE; - value = today->getPrice(pos); + value = today->getPrice(pos, direction); float mult = getCurrencyMultiplier(today->getCurrency(), config->currency, time(nullptr)); if(mult == 0) return PRICE_NO_VALUE; multiplier *= mult; @@ -422,7 +422,7 @@ PricesContainer* PriceService::fetchPrices(time_t t) { debugger->printf_P(PSTR("(PriceService) url: %s\n"), buf); PricesContainer* ret = new PricesContainer("EOE"); EntsoeA44Parser a44(ret); - if(retrieve(buf, &a44) && ret->hasPrice(0)) { + if(retrieve(buf, &a44) && ret->hasPrice(0, PRICE_DIRECTION_IMPORT)) { return ret; } else { delete ret; @@ -477,12 +477,17 @@ PricesContainer* PriceService::fetchPrices(time_t t) { AmsPriceV2Header* header = (AmsPriceV2Header*) (content-gcmRet); PricesContainer* ret = new PricesContainer(header->source); - ret->setup(header->resolutionInMinutes, header->hours); + ret->setup(header->resolutionInMinutes, header->numberOfPoints, header->differentExportPrices); ret->setCurrency(header->currency); int32_t* points = (int32_t*) &header[1]; for(uint8_t i = 0; i < header->numberOfPoints; i++) { - ret->setPrice(i, points[i]); + ret->setPrice(i, points[i], PRICE_DIRECTION_IMPORT); + } + if(header->differentExportPrices) { + for(uint8_t i = 0; i < header->numberOfPoints; i++) { + ret->setPrice(i, points[i], PRICE_DIRECTION_EXPORT); + } } lastError = 0; nextFetchDelayMinutes = 1; diff --git a/lib/PriceService/src/PricesContainer.cpp b/lib/PriceService/src/PricesContainer.cpp new file mode 100644 index 00000000..60804189 --- /dev/null +++ b/lib/PriceService/src/PricesContainer.cpp @@ -0,0 +1,67 @@ +#include "PricesContainer.h" +#include + +PricesContainer::PricesContainer(char* source) { + strncpy(this->source, source, 4); +} + +void PricesContainer::setup(uint8_t resolutionInMinutes, uint8_t numberOfPoints, bool differentExportPrices) { + this->resolutionInMinutes = resolutionInMinutes; + this->differentExportPrices = differentExportPrices; + this->numberOfPoints = numberOfPoints; + this->points = new int32_t[numberOfPoints * (differentExportPrices ? 2 : 1)]; + memset(this->points, PRICE_NO_VALUE * 10000, numberOfPoints * (differentExportPrices ? 2 : 1) * sizeof(int32_t)); +} + +char* PricesContainer::getSource() { + return this->source; +} + +void PricesContainer::setCurrency(char* currency) { + strncpy(this->currency, currency, 4); +} + +char* PricesContainer::getCurrency() { + return this->currency; +} + +uint8_t PricesContainer::getResolutionInMinutes() { + return this->resolutionInMinutes; +} + +uint8_t PricesContainer::getNumberOfPoints() { + return this->numberOfPoints; +} + +void PricesContainer::setPrice(uint8_t point, float value, uint8_t direction) { + if(direction == PRICE_DIRECTION_EXPORT && !differentExportPrices) { + return; // Export prices not supported + } + if(direction != PRICE_DIRECTION_EXPORT) { + points[point] = static_cast(value * 10000); + } + if(differentExportPrices && direction != PRICE_DIRECTION_IMPORT) { + points[point + numberOfPoints] = static_cast(value * 10000); + } +} + +bool PricesContainer::hasPrice(uint8_t point, uint8_t direction) { + float val = getPrice(point, direction); + return val != PRICE_NO_VALUE; +} + +float PricesContainer::getPrice(uint8_t point, uint8_t direction) { + if(differentExportPrices && direction == PRICE_DIRECTION_EXPORT) { + if(point < numberOfPoints) { + return static_cast(points[point + numberOfPoints]) / 10000.0f; + } + } + + if(differentExportPrices && direction == PRICE_DIRECTION_BOTH) return PRICE_NO_VALUE; // Can't get a price for both directions if the export prices are different + + if(point < numberOfPoints) { + return static_cast(points[point]) / 10000.0f; + } + + return PRICE_NO_VALUE; // Invalid point +} \ No newline at end of file diff --git a/lib/RawMqttHandler/src/RawMqttHandler.cpp b/lib/RawMqttHandler/src/RawMqttHandler.cpp index 4c4c5b13..23872af3 100644 --- a/lib/RawMqttHandler/src/RawMqttHandler.cpp +++ b/lib/RawMqttHandler/src/RawMqttHandler.cpp @@ -230,7 +230,7 @@ bool RawMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) bool RawMqttHandler::publishPrices(PriceService* ps) { if(topic.isEmpty() || !mqtt.connected()) return false; - if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE) + if(!ps->hasPrice()) return false; time_t now = time(nullptr); diff --git a/lib/SvelteUi/include/AmsWebServer.h b/lib/SvelteUi/include/AmsWebServer.h index 5266c93a..81a30e77 100644 --- a/lib/SvelteUi/include/AmsWebServer.h +++ b/lib/SvelteUi/include/AmsWebServer.h @@ -125,7 +125,10 @@ private: void dataJson(); void dayplotJson(); void monthplotJson(); - void energyPriceJson(); + void energyPriceJson(); // Deprecated + void importPriceJson(); + void exportPriceJson(); + void priceJson(uint8_t direction); void temperatureJson(); void tariffJson(); void realtimeJson(); diff --git a/lib/SvelteUi/src/AmsWebServer.cpp b/lib/SvelteUi/src/AmsWebServer.cpp index 63fb42ad..e1e30bc9 100644 --- a/lib/SvelteUi/src/AmsWebServer.cpp +++ b/lib/SvelteUi/src/AmsWebServer.cpp @@ -126,6 +126,8 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, AmsDa server.on(context + F("/dayplot.json"), HTTP_GET, std::bind(&AmsWebServer::dayplotJson, this)); server.on(context + F("/monthplot.json"), HTTP_GET, std::bind(&AmsWebServer::monthplotJson, this)); server.on(context + F("/energyprice.json"), HTTP_GET, std::bind(&AmsWebServer::energyPriceJson, this)); + server.on(context + F("/importprice.json"), HTTP_GET, std::bind(&AmsWebServer::importPriceJson, this)); + server.on(context + F("/exportprice.json"), HTTP_GET, std::bind(&AmsWebServer::exportPriceJson, this)); server.on(context + F("/temperature.json"), HTTP_GET, std::bind(&AmsWebServer::temperatureJson, this)); server.on(context + F("/tariff.json"), HTTP_GET, std::bind(&AmsWebServer::tariffJson, this)); server.on(context + F("/realtime.json"), HTTP_GET, std::bind(&AmsWebServer::realtimeJson, this)); @@ -579,8 +581,8 @@ void AmsWebServer::dataJson() { mqttStatus = 3; } - float price = ea->getPriceForHour(PRICE_DIRECTION_IMPORT, 0); - float exportPrice = ea->getPriceForHour(PRICE_DIRECTION_EXPORT, 0); + float price = ps == NULL ? PRICE_NO_VALUE : ps->getPrice(PRICE_DIRECTION_IMPORT); + float exportPrice = ps == NULL ? PRICE_NO_VALUE : ps->getPrice(PRICE_DIRECTION_EXPORT); String peaks = ""; for(uint8_t i = 1; i <= ea->getConfig()->hours; i++) { @@ -721,14 +723,19 @@ void AmsWebServer::energyPriceJson() { if(!checkSecurity(2)) return; + if(ps == NULL || !ps->hasPrice()) { + notFound(); + return; + } + float prices[36]; for(int i = 0; i < 36; i++) { - prices[i] = ps == NULL ? PRICE_NO_VALUE : ps->getValueForHour(PRICE_DIRECTION_IMPORT, i); + prices[i] = ps->getValueForHour(PRICE_DIRECTION_IMPORT, i); } uint16_t pos = snprintf_P(buf, BufferSize, PSTR("{\"currency\":\"%s\",\"source\":\"%s\""), - ps == NULL ? "" : ps->getCurrency(), - ps == NULL ? "" : ps->getSource() + ps->getCurrency(), + ps->getSource() ); for(uint8_t i = 0;i < 36; i++) { @@ -749,6 +756,56 @@ void AmsWebServer::energyPriceJson() { server.send(200, MIME_JSON, buf); } +void AmsWebServer::importPriceJson() { + priceJson(PRICE_DIRECTION_IMPORT); +} + +void AmsWebServer::exportPriceJson() { + priceJson(PRICE_DIRECTION_EXPORT); +} + +void AmsWebServer::priceJson(uint8_t direction) { + if(!checkSecurity(2)) + return; + + if(ps == NULL || !ps->hasPrice()) { + notFound(); + return; + } + + uint8_t numberOfPoints = ps->getNumberOfPointsAvailable(); + + float prices[numberOfPoints]; + for(int i = 0; i < numberOfPoints; i++) { + prices[i] = ps->getPricePoint(direction, i); + } + + uint16_t pos = snprintf_P(buf, BufferSize, PSTR("{\"currency\":\"%s\",\"source\":\"%s\",\"\"resolution\":%d,\"direction\":\"%s\",\"importExportPriceDifferent\":%s"), + ps->getCurrency(), + ps->getSource(), + ps->getResolutionInMinutes(), + direction == PRICE_DIRECTION_IMPORT ? "import" : direction == PRICE_DIRECTION_EXPORT ? "export" : "both", + ps->isExportPricesDifferentFromImport() ? "true" : "false" + ); + + for(uint8_t i = 0;i < numberOfPoints; i++) { + if(prices[i] == PRICE_NO_VALUE) { + pos += snprintf_P(buf+pos, BufferSize-pos, PSTR(",\"%02d\":null"), i); + } else { + pos += snprintf_P(buf+pos, BufferSize-pos, PSTR(",\"%02d\":%.4f"), i, prices[i]); + } + } + snprintf_P(buf+pos, BufferSize-pos, PSTR("}")); + + addConditionalCloudHeaders(); + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); + server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE); + server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF); + + server.setContentLength(strlen(buf)); + server.send(200, MIME_JSON, buf); +} + void AmsWebServer::temperatureJson() { if(!checkSecurity(2)) return; diff --git a/src/AmsToMqttBridge.cpp b/src/AmsToMqttBridge.cpp index e69f397d..7897638f 100644 --- a/src/AmsToMqttBridge.cpp +++ b/src/AmsToMqttBridge.cpp @@ -1545,7 +1545,7 @@ void MQTT_connect() { if(mqttHandler != NULL) { mqttHandler->connect(); mqttHandler->publishSystem(&hw, ps, &ea); - if(ps != NULL && ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) != PRICE_NO_VALUE) { + if(ps != NULL && ps->hasPrice()) { mqttHandler->publishPrices(ps); } }