Added support for 15 minute price resolution (#1031)

* 15min prices WIP

* WIP more changes for 15min prices

* More work on 15min pricing

* Fixed some errors

* Some changes after testing

* Graphical changes for 15min pricing

* Adjustments on MQTT handlers after switching to 15min prices

* Reverted some MQTT changes

* Adapted HA integration for 15min pricing

* Adapted JSON payload for 15min

* Adjustments during testing

* Set default price interval

* Fixed refresh of price graph when data changes

* Bugfixes

* Fixed some issues with raw payload

* Adjustments for meter timestamp from Kamstrup

* Updated readme

* Added detailed breakdown of payloads coming from Norwegian meters

* Minor changes relating to price

* Fixed byte alignment on price config

* Changes to support RC upgraders
This commit is contained in:
Gunnar Skjold
2025-11-13 15:10:54 +01:00
committed by GitHub
parent ffd8d46f2e
commit c648546b61
44 changed files with 1073 additions and 637 deletions

View File

@@ -15,28 +15,23 @@
#define DOCPOS_MEASUREMENTUNIT 2
#define DOCPOS_POSITION 3
#define DOCPOS_AMOUNT 4
#define DOCPOS_RESOLUTION 5
class EntsoeA44Parser: public Stream {
public:
EntsoeA44Parser();
EntsoeA44Parser(PricesContainer *container);
virtual ~EntsoeA44Parser();
char* getCurrency();
char* getMeasurementUnit();
float 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);
void get(PricesContainer*);
private:
char currency[4];
char measurementUnit[4];
float points[25];
PricesContainer *container;
float multiplier = 1.0;
char buf[64];
uint8_t pos = 0;

View File

@@ -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
@@ -57,10 +53,13 @@ struct PriceConfig {
uint8_t end_dayofmonth;
};
struct PricePart {
char name[32];
char description[32];
uint32_t value;
struct AmsPriceV2Header {
char currency[4];
char measurementUnit[4];
char source[4];
uint8_t resolutionInMinutes;
bool differentExportPrices;
uint8_t numberOfPoints;
};
class PriceService {
@@ -78,17 +77,25 @@ public:
char* getCurrency();
char* getArea();
char* getSource();
float getValueForHour(uint8_t direction, int8_t hour);
float getValueForHour(uint8_t direction, time_t ts, int8_t hour);
float getEnergyPriceForHour(uint8_t direction, time_t ts, int8_t hour);
uint8_t getResolutionInMinutes();
uint8_t getNumberOfPointsAvailable();
uint8_t getCurrentPricePointIndex();
bool isExportPricesDifferentFromImport();
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; }
float getCurrentPrice(uint8_t direction);
float getPricePoint(uint8_t direction, uint8_t point);
float getPriceForRelativeHour(uint8_t direction, int8_t hour); // If not 60min interval, average
std::vector<PriceConfig>& getPriceConfig();
void setPriceConfig(uint8_t index, PriceConfig &priceConfig);
void cropPriceConfig(uint8_t size);
PricePart getPricePart(uint8_t index);
int16_t getLastError();
bool load();
@@ -103,7 +110,7 @@ private:
PriceServiceConfig* config = NULL;
HTTPClient* http = NULL;
uint8_t currentDay = 0, currentHour = 0;
uint8_t currentDay = 0, currentPricePoint = 0;
uint8_t tomorrowFetchMinute = 15; // How many minutes over 13:00 should it fetch prices
uint8_t nextFetchDelayMinutes = 15;
uint64_t lastTodayFetch = 0;
@@ -132,5 +139,7 @@ private:
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 hour);
float getEnergyPricePoint(uint8_t direction, uint8_t point);
};
#endif

View File

@@ -4,15 +4,43 @@
*
*/
#include <stdint.h>
#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
struct PricesContainer {
char currency[4];
char measurementUnit[4];
int32_t points[25];
class PricesContainer {
public:
PricesContainer(char* source);
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 getNumberOfPoints();
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;
bool differentExportPrices;
uint8_t numberOfPoints;
int32_t *points;
};
#endif

View File

@@ -7,27 +7,14 @@
#include "EntsoeA44Parser.h"
#include "HardwareSerial.h"
EntsoeA44Parser::EntsoeA44Parser() {
for(int i = 0; i < 25; i++) points[i] = PRICE_NO_VALUE;
EntsoeA44Parser::EntsoeA44Parser(PricesContainer *container) {
this->container = container;
}
EntsoeA44Parser::~EntsoeA44Parser() {
}
char* EntsoeA44Parser::getCurrency() {
return currency;
}
char* EntsoeA44Parser::getMeasurementUnit() {
return measurementUnit;
}
float EntsoeA44Parser::getPoint(uint8_t position) {
if(position >= 25) return PRICE_NO_VALUE;
return points[position];
}
int EntsoeA44Parser::available() {
return 0;
}
@@ -57,7 +44,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
buf[pos++] = byte;
if(pos == 3) {
buf[pos++] = '\0';
memcpy(currency, buf, pos);
container->setCurrency(buf);
docPos = DOCPOS_SEEK;
pos = 0;
}
@@ -65,7 +52,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
buf[pos++] = byte;
if(pos == 3) {
buf[pos++] = '\0';
memcpy(measurementUnit, buf, pos);
if(strcmp_P(buf, PSTR("MWH"))) multiplier = 0.001;
docPos = DOCPOS_SEEK;
pos = 0;
}
@@ -73,7 +60,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
if(byte == '<') {
buf[pos] = '\0';
long pn = String(buf).toInt() - 1;
if(pn < 25) {
if(pn < container->getNumberOfPoints()) {
pointNum = pn;
}
docPos = DOCPOS_SEEK;
@@ -85,8 +72,25 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
if(byte == '<') {
buf[pos] = '\0';
float val = String(buf).toFloat();
for(uint8_t i = pointNum; i < 25; i++) {
points[i] = val;
for(uint8_t i = pointNum; i < container->getNumberOfPoints(); i++) {
container->setPrice(i, val * multiplier, PRICE_DIRECTION_IMPORT);
}
docPos = DOCPOS_SEEK;
pos = 0;
} else {
buf[pos++] = byte;
}
} else if(docPos == DOCPOS_RESOLUTION) {
if(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, PRICE_DIRECTION_IMPORT)) return 1;
if(strcmp_P(buf, PSTR("PT15M"))) {
container->setup(15, 100, false);
} else if(strcmp_P(buf, PSTR("PT60M"))) {
container->setup(60, 25, false);
}
docPos = DOCPOS_SEEK;
pos = 0;
@@ -101,15 +105,17 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
} else if(byte == '>') {
buf[pos++] = byte;
buf[pos] = '\0';
if(strcmp(buf, "<currency_Unit.name>") == 0) {
if(strcmp_P(buf, PSTR("<currency_Unit.name>")) == 0) {
docPos = DOCPOS_CURRENCY;
} else if(strcmp(buf, "<price_Measure_Unit.name>") == 0) {
} else if(strcmp(buf, PSTR("<price_Measure_Unit.name>")) == 0) {
docPos = DOCPOS_MEASUREMENTUNIT;
} else if(strcmp(buf, "<position>") == 0) {
} else if(strcmp(buf, PSTR("<position>")) == 0) {
docPos = DOCPOS_POSITION;
pointNum = 0xFF;
} else if(strcmp(buf, "<price.amount>") == 0) {
} else if(strcmp(buf, PSTR("<price.amount>")) == 0) {
docPos = DOCPOS_AMOUNT;
} else if(strcmp(buf, PSTR("<resolution>")) == 0) {
docPos = DOCPOS_RESOLUTION;
}
pos = 0;
} else {
@@ -118,15 +124,3 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
}
return 1;
}
void EntsoeA44Parser::get(PricesContainer* container) {
memset(container, 0, sizeof(*container));
strcpy(container->currency, currency);
strcpy(container->measurementUnit, measurementUnit);
strcpy(container->source, "EOE");
for(uint8_t i = 0; i < 25; i++) {
container->points[i] = points[i] == PRICE_NO_VALUE ? PRICE_NO_VALUE : points[i] * 10000;
}
}

View File

@@ -43,6 +43,10 @@ void PriceService::setup(PriceServiceConfig& config) {
this->config = new PriceServiceConfig();
}
memcpy(this->config, &config, sizeof(config));
if(this->config->resolutionInMinutes != 15 && this->config->resolutionInMinutes != 60) {
this->config->resolutionInMinutes = 60;
}
lastTodayFetch = lastTomorrowFetch = lastCurrencyFetch = 0;
if(today != NULL) delete today;
if(tomorrow != NULL) delete tomorrow;
@@ -91,55 +95,156 @@ char* PriceService::getArea() {
char* PriceService::getSource() {
if(this->today != NULL && this->tomorrow != NULL) {
if(strcmp(this->today->source, this->tomorrow->source) == 0) {
return this->today->source;
if(strcmp(this->today->getSource(), this->tomorrow->getSource()) == 0) {
return this->today->getSource();
} else {
return "MIX";
}
} else if(today != NULL) {
return this->today->source;
return this->today->getSource();
} else if(tomorrow != NULL) {
return this->tomorrow->source;
return this->tomorrow->getSource();
}
return "";
}
float PriceService::getValueForHour(uint8_t direction, int8_t hour) {
time_t cur = time(nullptr);
return getValueForHour(direction, cur, hour);
uint8_t PriceService::getResolutionInMinutes() {
return today != NULL ? today->getResolutionInMinutes() : 60;
}
float PriceService::getValueForHour(uint8_t direction, time_t ts, int8_t hour) {
float ret = getEnergyPriceForHour(direction, ts, hour);
if(ret == PRICE_NO_VALUE)
return ret;
uint8_t PriceService::getNumberOfPointsAvailable() {
if(today == NULL) return getResolutionInMinutes() == 15 ? 192 : 48;
if(tomorrow != NULL) return today->getNumberOfPoints() + tomorrow->getNumberOfPoints();
return today->getNumberOfPoints();
}
bool PriceService::isExportPricesDifferentFromImport() {
for (uint8_t i = 0; i < priceConfig.size(); i++) {
PriceConfig pc = priceConfig.at(i);
if(pc.direction != PRICE_DIRECTION_BOTH) {
return true;
}
}
return today != NULL && today->isExportPricesDifferentFromImport();
}
float PriceService::getPricePoint(uint8_t direction, uint8_t point) {
float value = getFixedPrice(direction, point * getResolutionInMinutes() / 60);
if(value == PRICE_NO_VALUE) value = getEnergyPricePoint(direction, point);
if(value == PRICE_NO_VALUE) return PRICE_NO_VALUE;
tmElements_t tm;
breakTime(tz->toLocal(ts + (hour * SECS_PER_HOUR)), tm);
time_t ts = time(nullptr);
breakTime(tz->toLocal(ts), tm);
tm.Hour = tm.Minute = tm.Second = 0;
breakTime(makeTime(tm) + (point * SECS_PER_MIN * getResolutionInMinutes()), tm);
for (uint8_t i = 0; i < priceConfig.size(); i++) {
PriceConfig pc = priceConfig.at(i);
if(pc.type == PRICE_TYPE_FIXED) continue;
if((pc.direction & direction) != direction) continue;
if(!timeIsInPeriod(tm, pc)) continue;
float pcVal = pc.value / 10000.0;
switch(pc.type) {
case PRICE_TYPE_ADD:
ret += pc.value / 10000.0;
value += pcVal;
break;
case PRICE_TYPE_SUBTRACT:
ret -= pc.value / 10000.0;
value -= pcVal;
break;
case PRICE_TYPE_PCT:
ret += ((pc.value / 10000.0) * ret) / 100.0;
value += (pcVal * value) / 100.0;
break;
}
}
return ret;
return value;
}
float PriceService::getEnergyPriceForHour(uint8_t direction, time_t ts, int8_t hour) {
float PriceService::getCurrentPrice(uint8_t direction) {
time_t ts = time(nullptr);
tmElements_t tm;
breakTime(tz->toLocal(ts + (hour * SECS_PER_HOUR)), tm);
breakTime(tz->toLocal(ts), tm);
uint8_t pos = getCurrentPricePointIndex();
return getPricePoint(direction, pos);
}
float PriceService::getEnergyPricePoint(uint8_t direction, uint8_t point) {
uint8_t pos = point;
float multiplier = 1.0;
uint8_t numberOfPointsToday = 24;
if(today != NULL) {
numberOfPointsToday = today->getNumberOfPoints();
}
float value = PRICE_NO_VALUE;
if(pos >= numberOfPointsToday) {
pos = pos - numberOfPointsToday;
if(tomorrow == NULL)
return PRICE_NO_VALUE;
if(pos >= tomorrow->getNumberOfPoints()) return PRICE_NO_VALUE;
if(!tomorrow->hasPrice(pos, direction))
return PRICE_NO_VALUE;
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, direction))
return PRICE_NO_VALUE;
value = today->getPrice(pos, direction);
float mult = getCurrencyMultiplier(today->getCurrency(), config->currency, time(nullptr));
if(mult == 0) return PRICE_NO_VALUE;
multiplier *= mult;
}
return value == PRICE_NO_VALUE ? PRICE_NO_VALUE : value * multiplier;
}
float PriceService::getPriceForRelativeHour(uint8_t direction, int8_t hour) {
time_t ts = time(nullptr);
tmElements_t tm;
breakTime(tz->toLocal(ts), tm);
int8_t targetHour = tm.Hour + hour;
tm.Hour = tm.Minute = tm.Second = 0;
time_t startOfDay = tz->toUTC(makeTime(tm));
if((ts + (hour * SECS_PER_HOUR)) < startOfDay) {
return PRICE_NO_VALUE;
}
if(getResolutionInMinutes() == 60) {
return getPricePoint(direction, targetHour);
}
float valueSum = 0.0f;
uint8_t valueCount = 0;
float indexIncrements = 60.0 / today->getResolutionInMinutes();
uint8_t priceMapIndexStart = (uint8_t) floor(indexIncrements * targetHour);
uint8_t priceMapIndexEnd = (uint8_t) ceil(indexIncrements * (targetHour+1));
for(uint8_t mi = priceMapIndexStart; mi < priceMapIndexEnd; mi++) {
float val = getPricePoint(direction, mi);
if(val == PRICE_NO_VALUE) continue;
valueSum += val;
valueCount++;
}
if(valueCount == 0) return PRICE_NO_VALUE;
return valueSum / valueCount;
}
float PriceService::getFixedPrice(uint8_t direction, int8_t hour) {
time_t ts = time(nullptr);
tmElements_t tm;
breakTime(tz->toLocal(ts), tm);
tm.Minute = 0;
tm.Second = 0;
breakTime(makeTime(tm) + (hour * SECS_PER_HOUR), tm);
float value = PRICE_NO_VALUE;
for (uint8_t i = 0; i < priceConfig.size(); i++) {
@@ -154,68 +259,7 @@ float PriceService::getEnergyPriceForHour(uint8_t direction, time_t ts, int8_t h
value += pc.value / 10000.0;
}
}
if(value != PRICE_NO_VALUE) return value;
int8_t pos = hour;
breakTime(entsoeTz->toLocal(ts), tm);
while(tm.Hour > 0) {
ts -= 3600;
breakTime(entsoeTz->toLocal(ts), tm);
pos++;
}
uint8_t hoursToday = 0;
uint8_t todayDate = tm.Day;
while(tm.Day == todayDate) {
ts += 3600;
breakTime(entsoeTz->toLocal(ts), tm);
hoursToday++;
}
uint8_t hoursTomorrow = 0;
uint8_t tomorrowDate = tm.Day;
while(tm.Day == tomorrowDate) {
ts += 3600;
breakTime(entsoeTz->toLocal(ts), tm);
hoursTomorrow++;
}
float multiplier = 1.0;
if(pos >= hoursToday) {
pos = pos - hoursToday;
if(pos >= hoursTomorrow) return PRICE_NO_VALUE;
if(tomorrow == NULL)
return PRICE_NO_VALUE;
if(tomorrow->points[pos] == PRICE_NO_VALUE)
return PRICE_NO_VALUE;
value = tomorrow->points[pos] / 10000.0;
if(strcmp(tomorrow->measurementUnit, "KWH") == 0) {
// Multiplier is 1
} else if(strcmp(tomorrow->measurementUnit, "MWH") == 0) {
multiplier *= 0.001;
} else {
return PRICE_NO_VALUE;
}
float mult = getCurrencyMultiplier(tomorrow->currency, 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->points[pos] == PRICE_NO_VALUE)
return PRICE_NO_VALUE;
value = today->points[pos] / 10000.0;
if(strcmp(today->measurementUnit, "KWH") == 0) {
// Multiplier is 1
} else if(strcmp(today->measurementUnit, "MWH") == 0) {
multiplier *= 0.001;
} else {
return PRICE_NO_VALUE;
}
float mult = getCurrencyMultiplier(today->currency, config->currency, time(nullptr));
if(mult == 0) return PRICE_NO_VALUE;
multiplier *= mult;
}
return value == PRICE_NO_VALUE ? PRICE_NO_VALUE : value * multiplier;
return value;
}
bool PriceService::loop() {
@@ -223,43 +267,59 @@ bool PriceService::loop() {
if(now < 10000) return false; // Grace period
time_t t = time(nullptr);
if(t < FirmwareVersion::BuildEpoch) return false;
#ifndef AMS2MQTT_PRICE_KEY
if(strlen(getToken()) == 0) {
if(t < FirmwareVersion::BuildEpoch) {
return false;
}
#endif
if(strlen(config->area) == 0)
return false;
if(strlen(config->currency) == 0)
return false;
tmElements_t tm;
breakTime(entsoeTz->toLocal(t), tm);
if(currentDay == 0) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("(PriceService) Day init\n"));
currentDay = tm.Day;
currentHour = tm.Hour;
currentPricePoint = getCurrentPricePointIndex();
}
if(currentDay != tm.Day) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("(PriceService) Day reset\n"));
if(today != NULL) delete today;
if(tomorrow != NULL) {
today = tomorrow;
tomorrow = NULL;
}
currentDay = tm.Day;
currentHour = tm.Hour;
currentPricePoint = getCurrentPricePointIndex();
return today != NULL || (!config->enabled && priceConfig.capacity() != 0); // Only trigger MQTT publish if we have todays prices.
} else if(currentHour != tm.Hour) {
currentHour = tm.Hour;
} 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.
}
if(!config->enabled)
return false;
#ifndef AMS2MQTT_PRICE_KEY
if(strlen(getToken()) == 0) {
return false;
}
#endif
if(strlen(config->area) == 0){
return false;
}
if(strlen(config->currency) == 0) {
return false;
}
bool readyToFetchForTomorrow = tomorrow == NULL && (tm.Hour > 13 || (tm.Hour == 13 && tm.Minute >= tomorrowFetchMinute)) && (lastTomorrowFetch == 0 || now - lastTomorrowFetch > (nextFetchDelayMinutes*60000));
if(today == NULL && (lastTodayFetch == 0 || now - lastTodayFetch > (nextFetchDelayMinutes*60000))) {
@@ -273,6 +333,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.
}
@@ -289,6 +350,7 @@ bool PriceService::loop() {
}
tomorrow = NULL;
}
currentPricePoint = getCurrentPricePointIndex();
return tomorrow != NULL;
}
@@ -434,24 +496,30 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("(PriceService) url: %s\n"), buf);
EntsoeA44Parser a44;
if(retrieve(buf, &a44) && a44.getPoint(0) != PRICE_NO_VALUE) {
PricesContainer* ret = new PricesContainer();
a44.get(ret);
PricesContainer* ret = new PricesContainer("EOE");
EntsoeA44Parser a44(ret);
if(retrieve(buf, &a44) && ret->hasPrice(0, PRICE_DIRECTION_IMPORT)) {
return ret;
} else {
delete ret;
return NULL;
}
} else if(hub) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("(PriceService) Going to fetch prices from hub\n"));
tmElements_t tm;
breakTime(entsoeTz->toLocal(t), tm);
String data;
snprintf_P(buf, BufferSize, PSTR("http://hub.amsleser.no/hub/price/%s/%d/%d/%d?currency=%s"),
snprintf_P(buf, BufferSize, PSTR("http://hub.amsleser.no/hub/price/%s/%d/%d/%d/pt%dm?currency=%s"),
config->area,
tm.Year+1970,
tm.Month,
tm.Day,
config->resolutionInMinutes,
config->currency
);
#if defined(AMS_REMOTE_DEBUG)
@@ -488,13 +556,37 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
GCMParser gcm(key, auth);
int8_t gcmRet = gcm.parse(content, ctx);
if(gcmRet > 0) {
PricesContainer* ret = new PricesContainer();
for(uint8_t i = 0; i < 25; i++) {
ret->points[i] = PRICE_NO_VALUE;
AmsPriceV2Header* header = (AmsPriceV2Header*) (content+gcmRet);
PricesContainer* ret = new PricesContainer(header->source);
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::DEBUG))
#endif
debugger->printf_P(PSTR("(PriceService) Setting up price container with pt%dm, %dpts, edi: %d\n"), header->resolutionInMinutes, header->numberOfPoints, header->differentExportPrices);
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++) {
int32_t intval = ntohl(points[i]);
float value = intval / 10000.0;
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("(PriceService) Import price position and value received: %d :: %.2f\n"), i, value);
ret->setPrice(i, value, PRICE_DIRECTION_IMPORT);
}
memcpy(ret, content+gcmRet, sizeof(*ret));
for(uint8_t i = 0; i < 25; i++) {
ret->points[i] = ntohl(ret->points[i]);
if(header->differentExportPrices) {
for(uint8_t i = 0; i < header->numberOfPoints; i++) {
int32_t intval = ntohl(points[i]);
float value = intval / 10000.0;
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("(PriceService) Export price position and value received: %d :: %.2f\n"), i, value);
ret->setPrice(i, value, PRICE_DIRECTION_EXPORT);
}
}
lastError = 0;
nextFetchDelayMinutes = 1;
@@ -658,4 +750,11 @@ bool PriceService::timeIsInPeriod(tmElements_t tm, PriceConfig pc) {
}
return makeTime(tms) <= makeTime(tm) && makeTime(tme) >= makeTime(tm);
}
uint8_t PriceService::getCurrentPricePointIndex() {
time_t ts = time(nullptr);
tmElements_t tm;
breakTime(tz->toLocal(ts), tm);
return ((tm.Hour * 60) + tm.Minute) / getResolutionInMinutes();
}

View File

@@ -0,0 +1,67 @@
#include "PricesContainer.h"
#include <cstring>
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<int32_t>(value * 10000);
}
if(differentExportPrices && direction != PRICE_DIRECTION_IMPORT) {
points[point + numberOfPoints] = static_cast<int32_t>(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<float>(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<float>(points[point]) / 10000.0f;
}
return PRICE_NO_VALUE; // Invalid point
}