diff --git a/src/AmsToMqttBridge.cpp b/src/AmsToMqttBridge.cpp index 57889a5d..18c3dbce 100644 --- a/src/AmsToMqttBridge.cpp +++ b/src/AmsToMqttBridge.cpp @@ -1069,7 +1069,7 @@ void handleCustomMqtt() { debugE_P(PSTR("Custom MQTT connector reporting error (%d)"), err); customMqttHandler->connect(); customMqttHandler->publishSystem(&hw, ps, &ea); - if(ps != NULL && ps->hasPrice()) { + if(ps != NULL && ps->hasAnyPrice()) { customMqttHandler->publishPrices(ps); } } @@ -1451,7 +1451,7 @@ void handlePriceService(unsigned long now) { if(config.isPriceServiceChanged()) { PriceServiceConfig price; - if(config.getPriceServiceConfig(price) && price.enabled && strlen(price.area) > 0) { + if(config.getPriceServiceConfig(price)) { if(ps == NULL) { ps = new PriceService(&Debug); ea.setPriceService(ps); @@ -1462,11 +1462,13 @@ void handlePriceService(unsigned long now) { } #endif } + // Kept alive even when fetching is disabled, as it also holds the fixed prices ps->setup(price); } else if(ps != NULL) { delete ps; ps = NULL; ws.setPriceService(NULL); + ea.setPriceService(NULL); } ws.setPriceSettings(price.area, price.currency); config.ackPriceServiceChange(); @@ -1983,7 +1985,7 @@ void MQTT_connect() { mqttHandler->setDataStorage(&ds); mqttHandler->connect(); mqttHandler->publishSystem(&hw, ps, &ea); - if(ps != NULL && ps->hasPrice()) { + if(ps != NULL && ps->hasAnyPrice()) { mqttHandler->publishPrices(ps); } } diff --git a/src/PriceService.cpp b/src/PriceService.cpp index 3092dfc0..1d0df789 100644 --- a/src/PriceService.cpp +++ b/src/PriceService.cpp @@ -73,6 +73,7 @@ void PriceService::setup(PriceServiceConfig& config) { #endif load(); + dynamicPriceNeedKnown = false; } void PriceService::setTimezone(Timezone* tz) { @@ -102,7 +103,7 @@ char* PriceService::getSource() { return this->today->getSource(); } else if(tomorrow != NULL) { return this->tomorrow->getSource(); - } else if(!this->config->enabled && this->priceConfig.capacity() != 0) { + } else if(hasFixedPrice()) { return "FIX"; // Fixed price } return ""; @@ -118,6 +119,68 @@ uint8_t PriceService::getNumberOfPointsAvailable() { return today->getNumberOfPoints(); } +bool PriceService::hasFixedPrice() { + for (uint8_t i = 0; i < priceConfig.size(); i++) { + if(priceConfig.at(i).type == PRICE_TYPE_FIXED) { + return true; + } + } + return false; +} + +bool PriceService::isDynamicPriceNeeded() { + if(!dynamicPriceNeedKnown) { + dynamicPriceNeeded = calculateDynamicPriceNeed(); + dynamicPriceNeedKnown = true; + #if defined(AMS_REMOTE_DEBUG) + if (debugger->isActive(RemoteDebug::INFO)) + #endif + debugger->printf_P(PSTR("(PriceService) Dynamic price is %sneeded\n"), dynamicPriceNeeded ? "" : "not "); + } + return dynamicPriceNeeded; +} + +bool PriceService::calculateDynamicPriceNeed() { + if(!hasFixedPrice()) return true; + + time_t ts = time(nullptr); + tmElements_t tm; + breakTime(entsoeTz->toLocal(ts), tm); + tm.Hour = tm.Minute = tm.Second = 0; + time_t startOfDay = entsoeTz->toUTC(makeTime(tm)); + + // Fixed price periods have hour granularity, and we can be asked for any hour of + // today and tomorrow, so those 48 hours are the whole horizon. Anchored at the start + // of today rather than at the current point, as the day cost is calculated backwards + // from midnight. + for(uint8_t hour = 0; hour < 48; hour++) { + breakTime(tz->toLocal(startOfDay + (hour * SECS_PER_HOUR)), tm); + tm.Minute = tm.Second = 0; + + uint8_t covered = 0; + for(uint8_t i = 0; i < priceConfig.size(); i++) { + PriceConfig pc = priceConfig.at(i); + if(pc.type != PRICE_TYPE_FIXED) continue; + if(!timeIsInPeriod(tm, pc)) continue; + covered |= pc.direction; + } + if((covered & PRICE_DIRECTION_BOTH) != PRICE_DIRECTION_BOTH) return true; + } + return false; +} + +int16_t PriceService::getLastKnownPricePoint(uint8_t direction) { + // Searching backwards, as the common case is that we know the price all the way + // to the end of the horizon and can return on the first probe. + uint8_t currentPricePointIndex = getCurrentPricePointIndex(); + for(int16_t point = getNumberOfPointsAvailable() - 1; point >= currentPricePointIndex; point--) { + if(getPricePoint(direction, point) != PRICE_NO_VALUE) { + return point; + } + } + return -1; +} + bool PriceService::isExportPricesDifferentFromImport() { for (uint8_t i = 0; i < priceConfig.size(); i++) { PriceConfig pc = priceConfig.at(i); @@ -237,7 +300,7 @@ float PriceService::getPriceForRelativeHour(uint8_t direction, int8_t hour) { return valueSum / valueCount; } -float PriceService::getFixedPrice(uint8_t direction, int8_t point) { +float PriceService::getFixedPrice(uint8_t direction, uint8_t point) { time_t ts = time(nullptr); tmElements_t tm; @@ -281,7 +344,9 @@ bool PriceService::loop() { #endif debugger->printf_P(PSTR("(PriceService) Day init\n")); currentDay = tm.Day; + dynamicPriceNeedKnown = false; currentPricePoint = getCurrentPricePointIndex(); + return hasFixedPrice(); // Publish a fixed price right away, we have nothing to wait for } if(currentDay != tm.Day) { @@ -295,20 +360,34 @@ bool PriceService::loop() { tomorrow = NULL; } currentDay = tm.Day; + dynamicPriceNeedKnown = false; currentPricePoint = getCurrentPricePointIndex(); - return today != NULL || (!config->enabled && priceConfig.capacity() != 0); // Only trigger MQTT publish if we have todays prices. + return today != NULL || hasFixedPrice(); // Only trigger MQTT publish if we have todays prices, or a fixed price to fall back on. } else if(currentPricePoint != getCurrentPricePointIndex()) { #if defined(AMS_REMOTE_DEBUG) if (debugger->isActive(RemoteDebug::INFO)) #endif debugger->printf_P(PSTR("(PriceService) Price point reset\n")); currentPricePoint = getCurrentPricePointIndex(); - return today != NULL || (!config->enabled && priceConfig.capacity() != 0); // Only trigger MQTT publish if we have todays prices. + return today != NULL || hasFixedPrice(); // Only trigger MQTT publish if we have todays prices, or a fixed price to fall back on. } if(!config->enabled) return false; + // A fixed price covering every hour in both directions makes the dynamic price + // irrelevant, so do not spend requests on fetching it + if(!isDynamicPriceNeeded()) { + if(today != NULL || tomorrow != NULL) { + if(today != NULL) delete today; + if(tomorrow != NULL) delete tomorrow; + today = tomorrow = NULL; + lastTodayFetch = lastTomorrowFetch = 0; + } + lastError = 0; + return false; + } + #ifndef AMS2MQTT_PRICE_KEY if(strlen(getToken()) == 0) { return false; @@ -335,7 +414,7 @@ bool PriceService::loop() { today = NULL; } currentPricePoint = getCurrentPricePointIndex(); - return today != NULL && !readyToFetchForTomorrow; // Only trigger MQTT publish if we have todays prices and we are not immediately ready to fetch price for tomorrow. + return today != NULL; // Publish as soon as we have todays prices. Tomorrows fetch publishes again if it succeeds. } // Prices for next day are published at 13:00 CE(S)T, but to avoid heavy server traffic at that time, we will @@ -649,6 +728,7 @@ std::vector& PriceService::getPriceConfig() { } void PriceService::setPriceConfig(uint8_t index, PriceConfig &priceConfig) { + dynamicPriceNeedKnown = false; stripNonAscii((uint8_t*) priceConfig.name, 32, true); if(this->priceConfig.capacity() != index+1) @@ -660,6 +740,7 @@ void PriceService::setPriceConfig(uint8_t index, PriceConfig &priceConfig) { } void PriceService::cropPriceConfig(uint8_t size) { + dynamicPriceNeedKnown = false; this->priceConfig.resize(size); this->priceConfig.shrink_to_fit(); diff --git a/src/PriceService.h b/src/PriceService.h index 24d2c642..0256aaba 100644 --- a/src/PriceService.h +++ b/src/PriceService.h @@ -87,6 +87,10 @@ class PriceService { bool hasPrice() { return hasPrice(PRICE_DIRECTION_IMPORT); } bool hasPrice(uint8_t direction) { return getCurrentPrice(direction) != PRICE_NO_VALUE; } bool hasPricePoint(uint8_t direction, int8_t point) { return getPricePoint(direction, point) != PRICE_NO_VALUE; } + bool hasAnyPrice() { return getLastKnownPricePoint(PRICE_DIRECTION_IMPORT) > -1 || getLastKnownPricePoint(PRICE_DIRECTION_EXPORT) > -1; } + bool hasFixedPrice(); + bool isDynamicPriceNeeded(); // False when a fixed price covers the whole horizon, making the dynamic price irrelevant + int16_t getLastKnownPricePoint(uint8_t direction); // Last point from the current one onwards that we know a price for, -1 if none float getCurrentPrice(uint8_t direction); float getPricePoint(uint8_t direction, uint8_t point); @@ -132,11 +136,15 @@ class PriceService { int16_t lastError = 0; + bool dynamicPriceNeeded = true; + bool dynamicPriceNeedKnown = false; + bool calculateDynamicPriceNeed(); + PricesContainer* fetchPrices(time_t); bool retrieve(const char* url, Stream* doc); float getCurrencyMultiplier(const char* from, const char* to, time_t t); bool timeIsInPeriod(tmElements_t tm, PriceConfig pc); - float getFixedPrice(uint8_t direction, int8_t point); + float getFixedPrice(uint8_t direction, uint8_t point); float getEnergyPricePoint(uint8_t direction, uint8_t point); }; #endif diff --git a/src/mqtt/HomeAssistantMqttHandler.cpp b/src/mqtt/HomeAssistantMqttHandler.cpp index 85d94cac..12e7e098 100644 --- a/src/mqtt/HomeAssistantMqttHandler.cpp +++ b/src/mqtt/HomeAssistantMqttHandler.cpp @@ -331,7 +331,7 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) { if(pubTopic[0] == '\0' || !connected()) return false; - if(!ps->hasPrice()) + if(!ps->hasAnyPrice()) return false; publishPriceSensors(ps); @@ -347,7 +347,7 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) { float val = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i); values[i] = val; - if(val == PRICE_NO_VALUE) break; + if(val == PRICE_NO_VALUE) continue; // A hole, the price for this hour depends on a dynamic price we do not have if(val < min) min = val; if(val > max) max = val; @@ -700,13 +700,14 @@ void HomeAssistantMqttHandler::publishPriceSensors(PriceService* ps) { } uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex(); - uint8_t numberOfPoints = ps->getNumberOfPointsAvailable(); + // Discover sensors all the way out to the last point we know a price for. Points in + // between can still be unknown, if they depend on a dynamic price we do not have. + int16_t lastImportPoint = ps->getLastKnownPricePoint(PRICE_DIRECTION_IMPORT); + int16_t lastExportPoint = ps->getLastKnownPricePoint(PRICE_DIRECTION_EXPORT); - if(priceImportInit < numberOfPoints-currentPricePointIndex) { + if(priceImportInit < lastImportPoint-currentPricePointIndex+1) { uint8_t importPriceSensorNo = 0; - for(int pricePointIndex = currentPricePointIndex; pricePointIndex < numberOfPoints; pricePointIndex++) { - float val = ps->getPricePoint(PRICE_DIRECTION_IMPORT, pricePointIndex); - if(val == PRICE_NO_VALUE) break; + for(int pricePointIndex = currentPricePointIndex; pricePointIndex <= lastImportPoint; pricePointIndex++) { if(importPriceSensorNo < priceImportInit) { importPriceSensorNo++; continue; @@ -732,7 +733,7 @@ void HomeAssistantMqttHandler::publishPriceSensors(PriceService* ps) { importPriceSensorNo == 0 ? "Current import price" : name, "/prices", path, - resolution * 60 + 300, + resolution * 60 * 2 + 300, uom.c_str(), "monetary", importPriceSensorNo == 0 ? "total" : "", @@ -744,11 +745,9 @@ void HomeAssistantMqttHandler::publishPriceSensors(PriceService* ps) { } } - if(priceExportInit < numberOfPoints-currentPricePointIndex) { + if(priceExportInit < lastExportPoint-currentPricePointIndex+1) { uint8_t exportPriceSensorNo = 0; - for(int pricePointIndex = currentPricePointIndex; pricePointIndex < numberOfPoints; pricePointIndex++) { - float val = ps->getPricePoint(PRICE_DIRECTION_EXPORT, pricePointIndex); - if(val == PRICE_NO_VALUE) break; + for(int pricePointIndex = currentPricePointIndex; pricePointIndex <= lastExportPoint; pricePointIndex++) { if(exportPriceSensorNo < priceExportInit) { exportPriceSensorNo++; continue; @@ -774,7 +773,7 @@ void HomeAssistantMqttHandler::publishPriceSensors(PriceService* ps) { exportPriceSensorNo == 0 ? "Current export price" : name, "/prices", path, - resolution * 60 + 300, + resolution * 60 * 2 + 300, uom.c_str(), "monetary", exportPriceSensorNo == 0 ? "total" : "", diff --git a/src/mqtt/JsonMqttHandler.cpp b/src/mqtt/JsonMqttHandler.cpp index 3c9cc0ec..04c47679 100644 --- a/src/mqtt/JsonMqttHandler.cpp +++ b/src/mqtt/JsonMqttHandler.cpp @@ -305,7 +305,7 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) bool JsonMqttHandler::publishPrices(PriceService* ps) { if(strlen(mqttConfig.publishTopic) == 0 || !connected()) return false; - if(!ps->hasPrice()) + if(!ps->hasAnyPrice()) return false; time_t now = time(nullptr); @@ -319,7 +319,7 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) { float val = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i); values[i] = val; - if(val == PRICE_NO_VALUE) break; + if(val == PRICE_NO_VALUE) continue; // A hole, the price for this hour depends on a dynamic price we do not have if(val < min) min = val; if(val > max) max = val; diff --git a/src/mqtt/RawMqttHandler.cpp b/src/mqtt/RawMqttHandler.cpp index 751a8ec2..bedc5659 100644 --- a/src/mqtt/RawMqttHandler.cpp +++ b/src/mqtt/RawMqttHandler.cpp @@ -243,7 +243,7 @@ bool RawMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) bool RawMqttHandler::publishPrices(PriceService* ps) { if(topic.isEmpty() || !connected()) return false; - if(!ps->hasPrice()) + if(!ps->hasAnyPrice()) return false; time_t now = time(nullptr); @@ -258,7 +258,7 @@ bool RawMqttHandler::publishPrices(PriceService* ps) { values[i] = val; if(i > 23) continue; - if(val == PRICE_NO_VALUE) break; + if(val == PRICE_NO_VALUE) continue; // A hole, the price for this hour depends on a dynamic price we do not have if(val < min) min = val; if(val > max) max = val;