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

@@ -127,6 +127,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));
@@ -584,8 +586,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->getCurrentPrice(PRICE_DIRECTION_IMPORT);
float exportPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_EXPORT);
String peaks = "";
for(uint8_t i = 1; i <= ea->getConfig()->hours; i++) {
@@ -712,18 +714,24 @@ void AmsWebServer::monthplotJson() {
}
}
// Deprecated
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->getPriceForRelativeHour(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++) {
@@ -744,6 +752,58 @@ 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);
}
snprintf_P(buf, BufferSize, PSTR("{\"currency\":\"%s\",\"source\":\"%s\",\"resolution\":%d,\"direction\":\"%s\",\"importExportPriceDifferent\":%s,\"prices\":["),
ps->getCurrency(),
ps->getSource(),
ps->getResolutionInMinutes(),
direction == PRICE_DIRECTION_IMPORT ? "import" : direction == PRICE_DIRECTION_EXPORT ? "export" : "both",
ps->isExportPricesDifferentFromImport() ? "true" : "false"
);
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(CONTENT_LENGTH_UNKNOWN);
server.send(200, MIME_JSON, buf);
for(uint8_t i = 0;i < numberOfPoints; i++) {
if(prices[i] == PRICE_NO_VALUE) {
snprintf_P(buf, BufferSize, PSTR("%snull"), i == 0 ? "" : ",");
server.sendContent(buf);
} else {
snprintf_P(buf, BufferSize, PSTR("%s%.4f"), i == 0 ? "" : ",", prices[i]);
server.sendContent(buf);
}
}
server.sendContent_P(PSTR("]}"));
}
void AmsWebServer::temperatureJson() {
if(!checkSecurity(2))
return;
@@ -988,7 +1048,8 @@ void AmsWebServer::configurationJson() {
price.enabled ? "true" : "false",
price.entsoeToken,
price.area,
price.currency
price.currency,
price.resolutionInMinutes
);
server.sendContent(buf);
snprintf_P(buf, BufferSize, CONF_DEBUG_JSON,
@@ -1333,18 +1394,20 @@ void AmsWebServer::handleSave() {
if(server.hasArg(F("w")) && server.arg(F("w")) == F("true")) {
long mode = server.arg(F("nc")).toInt();
if(mode > 0 && mode < 3) {
if(mode > 0) {
NetworkConfig network;
config->getNetworkConfig(network);
network.mode = mode;
strcpy(network.ssid, server.arg(F("ws")).c_str());
String psk = server.arg(F("wp"));
if(!psk.equals("***")) {
strcpy(network.psk, psk.c_str());
if(mode < 3) {
strcpy(network.ssid, server.arg(F("ws")).c_str());
String psk = server.arg(F("wp"));
if(!psk.equals("***")) {
strcpy(network.psk, psk.c_str());
}
network.power = server.arg(F("ww")).toFloat() * 10;
network.sleep = server.arg(F("wz")).toInt();
network.use11b = server.hasArg(F("wb")) && server.arg(F("wb")) == F("true");
}
network.power = server.arg(F("ww")).toFloat() * 10;
network.sleep = server.arg(F("wz")).toInt();
network.use11b = server.hasArg(F("wb")) && server.arg(F("wb")) == F("true");
if(server.hasArg(F("nm"))) {
if(server.arg(F("nm")) == "static") {
@@ -1564,6 +1627,7 @@ void AmsWebServer::handleSave() {
strcpy(price.entsoeToken, server.arg(F("pt")).c_str());
strcpy(price.area, priceRegion.c_str());
strcpy(price.currency, server.arg(F("pc")).c_str());
price.resolutionInMinutes = server.arg(F("pm")).toInt();
config->setPriceServiceConfig(price);
}