mirror of
https://github.com/UtilitechAS/amsreader-firmware.git
synced 2026-01-28 13:08:57 +00:00
Added support for retrieving energy price from ENTSO-E API
This commit is contained in:
60
src/entsoe/DnbCurrParser.cpp
Normal file
60
src/entsoe/DnbCurrParser.cpp
Normal file
@@ -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, "<Obs", 4) == 0) {
|
||||
for(int i = 0; i < pos; i++) {
|
||||
if(strncmp(buf+i, "OBS_VALUE=\"", 11) == 0) {
|
||||
pos = i + 11;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for(int i = 0; i < 16; i++) {
|
||||
uint8_t b = buf[pos+i];
|
||||
if(b == '"') {
|
||||
buf[pos+i] = '\0';
|
||||
break;
|
||||
}
|
||||
}
|
||||
value = String(buf+pos).toDouble();
|
||||
}
|
||||
pos = 0;
|
||||
} else {
|
||||
buf[pos++] = byte;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
25
src/entsoe/DnbCurrParser.h
Normal file
25
src/entsoe/DnbCurrParser.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#ifndef _DNBCURRPARSER_H
|
||||
#define _DNBCURRPARSER_H
|
||||
|
||||
#include "Stream.h"
|
||||
|
||||
class DnbCurrParser: public Stream {
|
||||
public:
|
||||
double getValue();
|
||||
|
||||
int available();
|
||||
int read();
|
||||
int peek();
|
||||
void flush();
|
||||
size_t write(const uint8_t *buffer, size_t size);
|
||||
size_t write(uint8_t);
|
||||
|
||||
private:
|
||||
double value = 1.0;
|
||||
|
||||
char buf[256];
|
||||
uint8_t pos = 0;
|
||||
uint8_t mode = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
102
src/entsoe/EntsoeA44Parser.cpp
Normal file
102
src/entsoe/EntsoeA44Parser.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
#include "EntsoeA44Parser.h"
|
||||
#include "HardwareSerial.h"
|
||||
|
||||
EntsoeA44Parser::EntsoeA44Parser() {
|
||||
for(int i = 0; i < 24; i++) points[i] = 0.0;
|
||||
}
|
||||
|
||||
char* EntsoeA44Parser::getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
char* EntsoeA44Parser::getMeasurementUnit() {
|
||||
return measurementUnit;
|
||||
}
|
||||
|
||||
double EntsoeA44Parser::getPoint(uint8_t position) {
|
||||
return points[position];
|
||||
}
|
||||
|
||||
int EntsoeA44Parser::available() {
|
||||
|
||||
}
|
||||
|
||||
int EntsoeA44Parser::read() {
|
||||
|
||||
}
|
||||
|
||||
int EntsoeA44Parser::peek() {
|
||||
|
||||
}
|
||||
|
||||
void EntsoeA44Parser::flush() {
|
||||
|
||||
}
|
||||
|
||||
size_t EntsoeA44Parser::write(const uint8_t *buffer, size_t size) {
|
||||
for(int i = 0; i < size; i++) {
|
||||
write(buffer[i]);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
if(docPos == DOCPOS_CURRENCY) {
|
||||
buf[pos++] = byte;
|
||||
if(pos == 3) {
|
||||
buf[pos++] = '\0';
|
||||
memcpy(currency, buf, pos);
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
}
|
||||
} else if(docPos == DOCPOS_MEASUREMENTUNIT) {
|
||||
buf[pos++] = byte;
|
||||
if(pos == 3) {
|
||||
buf[pos++] = '\0';
|
||||
memcpy(measurementUnit, buf, pos);
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
}
|
||||
} else if(docPos == DOCPOS_POSITION) {
|
||||
if(byte == '<') {
|
||||
buf[pos] = '\0';
|
||||
pointNum = String(buf).toInt() - 1;
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
} else {
|
||||
buf[pos++] = byte;
|
||||
}
|
||||
} else if(docPos == DOCPOS_AMOUNT) {
|
||||
if(byte == '<') {
|
||||
buf[pos] = '\0';
|
||||
points[pointNum] = String(buf).toDouble();
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
} else {
|
||||
buf[pos++] = byte;
|
||||
}
|
||||
} else {
|
||||
if(pos == 0) {
|
||||
if(byte == '<') {
|
||||
buf[pos++] = byte;
|
||||
}
|
||||
} else if(byte == '>') {
|
||||
buf[pos++] = byte;
|
||||
buf[pos] = '\0';
|
||||
if(strcmp(buf, "<currency_Unit.name>") == 0) {
|
||||
docPos = DOCPOS_CURRENCY;
|
||||
} else if(strcmp(buf, "<price_Measure_Unit.name>") == 0) {
|
||||
docPos = DOCPOS_MEASUREMENTUNIT;
|
||||
} else if(strcmp(buf, "<position>") == 0) {
|
||||
docPos = DOCPOS_POSITION;
|
||||
pointNum = 0xFF;
|
||||
} else if(strcmp(buf, "<price.amount>") == 0) {
|
||||
docPos = DOCPOS_AMOUNT;
|
||||
}
|
||||
pos = 0;
|
||||
} else {
|
||||
buf[pos++] = byte;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
38
src/entsoe/EntsoeA44Parser.h
Normal file
38
src/entsoe/EntsoeA44Parser.h
Normal file
@@ -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
|
||||
261
src/entsoe/EntsoeApi.cpp
Normal file
261
src/entsoe/EntsoeApi.cpp
Normal file
@@ -0,0 +1,261 @@
|
||||
#include "EntsoeApi.h"
|
||||
#include <EEPROM.h>
|
||||
#include "Uptime.h"
|
||||
#include "Time.h"
|
||||
#include "DnbCurrParser.h"
|
||||
|
||||
#if defined(ESP8266)
|
||||
#include <ESP8266HTTPClient.h>
|
||||
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
|
||||
#include <HTTPClient.h>
|
||||
#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);
|
||||
}
|
||||
50
src/entsoe/EntsoeApi.h
Normal file
50
src/entsoe/EntsoeApi.h
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user