Compare commits

...

14 Commits

Author SHA1 Message Date
Gunnar Skjold
dd87d70876 Free value on fuse size and production capacity 2022-01-28 18:29:50 +01:00
Gunnar Skjold
52992f09ee Stability adjustments 2022-01-28 17:41:48 +01:00
Gunnar Skjold
6aa02d54c8 Improvements for ENTSO-E 2022-01-23 17:52:10 +01:00
Gunnar Skjold
9dbf9137c7 Merge branch 'master' of github.com:gskjold/AmsToMqttBridge 2022-01-21 17:57:35 +01:00
Gunnar Skjold
c0a9a01b41 Fixed uint problem with current 2022-01-21 17:57:01 +01:00
Gunnar Skjold
7b203b196f Fixed voltage and current labels for esp8266 2022-01-21 17:50:02 +01:00
Gunnar Skjold
50c06e2cfe Removed ap pin from generic ESP32 2022-01-16 19:38:35 +01:00
Gunnar Skjold
e4d4753181 Fixed incorrect labels for IT/TT current 2022-01-16 17:24:57 +01:00
Gunnar Skjold
04daf551fb Fixed incorrect time diff for price fetch 2022-01-16 17:04:58 +01:00
Gunnar Skjold
4b51f0f235 Only update exchange rate once per day 2022-01-16 10:50:08 +01:00
Gunnar Skjold
5e4ccca663 Fetch prices between 13:30 and 14:00 2022-01-16 10:37:08 +01:00
Gunnar Skjold
6b0ec39759 Various updates 2022-01-16 09:58:13 +01:00
Gunnar Skjold
1f7d845a32 Removed debugging 2022-01-14 07:13:43 +01:00
Gunnar Skjold
6de5b719f3 Various changes 2022-01-14 07:12:08 +01:00
16 changed files with 279 additions and 138 deletions

View File

@@ -232,6 +232,8 @@ bool AmsConfiguration::getDebugConfig(DebugConfig& config) {
}
bool AmsConfiguration::setDebugConfig(DebugConfig& config) {
if(!config.serial && !config.telnet)
config.level = 5; // Force error level when debug is disabled
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_DEBUG_START, config);
bool ret = EEPROM.commit();
@@ -449,6 +451,9 @@ bool AmsConfiguration::getEntsoeConfig(EntsoeConfig& config) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_ENTSOE_START, config);
EEPROM.end();
if(strlen(config.token) != 0 && strlen(config.token) != 36) {
clearEntsoe(config);
}
return true;
} else {
return false;

View File

@@ -198,6 +198,9 @@ bool AmsDataStorage::update(AmsData* data) {
float iph = im / hrs;
float eph = ex / hrs;
// There is something wacky going on when it ends up here. The total value (im) is way way lower than it should be, which in
// turn causes low values for all estimates. And then when it returns to the normal case above, the value is waaay higher.
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Since last month update, hours: %.1f, import: %d (%.2f/hr), export: %d (%.2f/hr)\n", hrs, im, iph, ex, eph);
}
@@ -210,7 +213,7 @@ bool AmsDataStorage::update(AmsData* data) {
}
month.lastMeterReadTime = month.lastMeterReadTime - (tm.Hour * 3600) - (tm.Minute * 60) - tm.Second;
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Last month read after resetting to midnight: %lu", month.lastMeterReadTime);
debugger->printf("(AmsDataStorage) Last month read after resetting to midnight: %lu\n", month.lastMeterReadTime);
}
if(tz != NULL) {

View File

@@ -13,10 +13,12 @@
*/
#if defined(ESP8266)
ADC_MODE(ADC_VCC);
#else if defined(ESP32)
#endif
#if defined(ESP32)
#include <esp_task_wdt.h>
#endif
#define WDT_TIMEOUT 10
#define WDT_TIMEOUT 30
#include "AmsToMqttBridge.h"
#include "AmsStorage.h"
@@ -135,12 +137,14 @@ void setup() {
hw.ledBlink(LED_GREEN, 1);
hw.ledBlink(LED_BLUE, 1);
#if defined(ESP32)
EntsoeConfig entsoe;
if(config.getEntsoeConfig(entsoe) && strlen(entsoe.token) > 0) {
eapi = new EntsoeApi(&Debug);
eapi->setup(entsoe);
ws.setEntsoeApi(eapi);
}
#endif
bool shared = false;
config.getMeterConfig(meterConfig);
@@ -434,6 +438,7 @@ void loop() {
mqtt->disconnect();
}
#if defined(ESP32)
if(eapi != NULL && ntpEnabled) {
if(eapi->loop() && mqtt != NULL && mqttHandler != NULL && mqtt->connected()) {
mqttHandler->publishPrices(eapi);
@@ -451,10 +456,11 @@ void loop() {
} else if(eapi != NULL) {
delete eapi;
eapi = NULL;
ws.setEntsoeApi(eapi);
ws.setEntsoeApi(NULL);
}
config.ackEntsoeChange();
}
#endif
ws.loop();
}
if(mqtt != NULL) { // Run loop regardless, to let MQTT do its work.
@@ -553,8 +559,10 @@ void setupHanPort(uint8_t pin, uint32_t baud, uint8_t parityOrdinal, bool invert
#if defined(ESP32)
hwSerial->begin(baud, serialConfig, -1, -1, invert);
hwSerial->setRxBufferSize(768);
#else
hwSerial->begin(baud, serialConfig, SERIAL_FULL, 1, invert);
hwSerial->setRxBufferSize(768);
#endif
#if defined(ESP8266)
@@ -699,7 +707,6 @@ bool readHanPort() {
while(hanSerial->available() && pos == HDLC_FRAME_INCOMPLETE) {
buf[len++] = hanSerial->read();
pos = HDLC_validate((uint8_t *) buf, len, hc, &timestamp);
delay(1);
}
if(len > 0) {
if(len >= BUF_SIZE) {
@@ -749,7 +756,7 @@ bool readHanPort() {
debugD("Frame dump (%db):", len);
debugPrint(buf, 0, len);
}
if(hc != NULL && Debug.isActive(RemoteDebug::DEBUG)) {
if(hc != NULL && Debug.isActive(RemoteDebug::VERBOSE)) {
debugD("System title:");
debugPrint(hc->system_title, 0, 8);
debugD("Initialization vector:");
@@ -762,7 +769,7 @@ bool readHanPort() {
len = 0;
while(hanSerial->available()) hanSerial->read();
if(pos > 0) {
debugI("Valid data, start at byte %d", pos);
debugD("Valid data, start at byte %d", pos);
data = IEC6205675(((char *) (buf)) + pos, meterState.getMeterType(), meterConfig.distributionSystem, timestamp, hc);
} else {
if(Debug.isActive(RemoteDebug::WARNING)) {
@@ -967,7 +974,9 @@ void WiFi_connect() {
} else if(dns1.toString().isEmpty()) {
dns2.fromString("208.67.220.220"); // Add OpenDNS as second by default if nothing is configured
}
WiFi.config(ip, gw, sn, dns1, dns2);
if(!WiFi.config(ip, gw, sn, dns1, dns2)) {
debugE("Static IP configuration is invalid, not using");
}
} else {
#if defined(ESP32)
// This trick does not work anymore...

View File

@@ -3,7 +3,6 @@
#include "Timezone.h"
IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, uint8_t distributionSystem, CosemDateTime packageTimestamp, HDLCConfig* hc) {
uint32_t ui;
double val;
char str[64];
@@ -13,8 +12,8 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, uint8_t distribution
this->packageTimestamp = getTimestamp(packageTimestamp);
ui = getNumber(AMS_OBIS_ACTIVE_IMPORT, sizeof(AMS_OBIS_ACTIVE_IMPORT), ((char *) (d)));
if(ui == 0xFFFFFFFF) {
val = getNumber(AMS_OBIS_ACTIVE_IMPORT, sizeof(AMS_OBIS_ACTIVE_IMPORT), ((char *) (d)));
if(val == NOVALUE) {
CosemData* data = getCosemDataAt(1, ((char *) (d)));
// Kaifa special case...
@@ -137,7 +136,7 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, uint8_t distribution
// Kaifa end
} else {
listType = 1;
activeImportPower = ui;
activeImportPower = val;
meterType = AmsTypeUnknown;
CosemData* version = findObis(AMS_OBIS_VERSION, sizeof(AMS_OBIS_VERSION), d);
@@ -166,95 +165,96 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, uint8_t distribution
this->packageTimestamp = this->packageTimestamp > 0 ? tz.toUTC(this->packageTimestamp) : 0;
}
ui = getString(AMS_OBIS_VERSION, sizeof(AMS_OBIS_VERSION), ((char *) (d)), str);
if(ui > 0) {
uint8_t str_len = 0;
str_len = getString(AMS_OBIS_VERSION, sizeof(AMS_OBIS_VERSION), ((char *) (d)), str);
if(str_len > 0) {
listId = String(str);
}
ui = getNumber(AMS_OBIS_ACTIVE_EXPORT, sizeof(AMS_OBIS_ACTIVE_EXPORT), ((char *) (d)));
if(ui != 0xFFFFFFFF) {
activeExportPower = ui;
val = getNumber(AMS_OBIS_ACTIVE_EXPORT, sizeof(AMS_OBIS_ACTIVE_EXPORT), ((char *) (d)));
if(val != NOVALUE) {
activeExportPower = val;
}
ui = getNumber(AMS_OBIS_REACTIVE_IMPORT, sizeof(AMS_OBIS_REACTIVE_IMPORT), ((char *) (d)));
if(ui != 0xFFFFFFFF) {
reactiveImportPower = ui;
val = getNumber(AMS_OBIS_REACTIVE_IMPORT, sizeof(AMS_OBIS_REACTIVE_IMPORT), ((char *) (d)));
if(val != NOVALUE) {
reactiveImportPower = val;
}
ui = getNumber(AMS_OBIS_REACTIVE_EXPORT, sizeof(AMS_OBIS_REACTIVE_EXPORT), ((char *) (d)));
if(ui != 0xFFFFFFFF) {
reactiveExportPower = ui;
val = getNumber(AMS_OBIS_REACTIVE_EXPORT, sizeof(AMS_OBIS_REACTIVE_EXPORT), ((char *) (d)));
if(val != NOVALUE) {
reactiveExportPower = val;
}
val = getNumber(AMS_OBIS_VOLTAGE_L1, sizeof(AMS_OBIS_VOLTAGE_L1), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 2;
l1voltage = val;
}
val = getNumber(AMS_OBIS_VOLTAGE_L2, sizeof(AMS_OBIS_VOLTAGE_L2), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 2;
l2voltage = val;
}
val = getNumber(AMS_OBIS_VOLTAGE_L3, sizeof(AMS_OBIS_VOLTAGE_L3), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 2;
l3voltage = val;
}
val = getNumber(AMS_OBIS_CURRENT_L1, sizeof(AMS_OBIS_CURRENT_L1), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 2;
l1current = val;
}
val = getNumber(AMS_OBIS_CURRENT_L2, sizeof(AMS_OBIS_CURRENT_L2), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 2;
l2current = val;
}
val = getNumber(AMS_OBIS_CURRENT_L3, sizeof(AMS_OBIS_CURRENT_L3), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 2;
l3current = val;
}
val = getNumber(AMS_OBIS_ACTIVE_IMPORT_COUNT, sizeof(AMS_OBIS_ACTIVE_IMPORT_COUNT), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 3;
activeImportCounter = val / 1000.0;
}
val = getNumber(AMS_OBIS_ACTIVE_EXPORT_COUNT, sizeof(AMS_OBIS_ACTIVE_EXPORT_COUNT), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 3;
activeExportCounter = val / 1000.0;
}
val = getNumber(AMS_OBIS_REACTIVE_IMPORT_COUNT, sizeof(AMS_OBIS_REACTIVE_IMPORT_COUNT), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 3;
reactiveImportCounter = val / 1000.0;
}
val = getNumber(AMS_OBIS_REACTIVE_EXPORT_COUNT, sizeof(AMS_OBIS_REACTIVE_EXPORT_COUNT), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
listType = 3;
reactiveExportCounter = val / 1000.0;
}
ui = getString(AMS_OBIS_METER_MODEL, sizeof(AMS_OBIS_METER_MODEL), ((char *) (d)), str);
if(ui > 0) {
str_len = getString(AMS_OBIS_METER_MODEL, sizeof(AMS_OBIS_METER_MODEL), ((char *) (d)), str);
if(str_len > 0) {
meterModel = String(str);
} else {
ui = getString(AMS_OBIS_METER_MODEL_2, sizeof(AMS_OBIS_METER_MODEL_2), ((char *) (d)), str);
if(ui > 0) {
str_len = getString(AMS_OBIS_METER_MODEL_2, sizeof(AMS_OBIS_METER_MODEL_2), ((char *) (d)), str);
if(str_len > 0) {
meterModel = String(str);
}
}
ui = getString(AMS_OBIS_METER_ID, sizeof(AMS_OBIS_METER_ID), ((char *) (d)), str);
if(ui > 0) {
str_len = getString(AMS_OBIS_METER_ID, sizeof(AMS_OBIS_METER_ID), ((char *) (d)), str);
if(str_len > 0) {
meterId = String(str);
} else {
ui = getString(AMS_OBIS_METER_ID_2, sizeof(AMS_OBIS_METER_ID_2), ((char *) (d)), str);
if(ui > 0) {
str_len = getString(AMS_OBIS_METER_ID_2, sizeof(AMS_OBIS_METER_ID_2), ((char *) (d)), str);
if(str_len > 0) {
meterId = String(str);
}
}
@@ -271,19 +271,19 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, uint8_t distribution
}
val = getNumber(AMS_OBIS_POWER_FACTOR, sizeof(AMS_OBIS_POWER_FACTOR), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
powerFactor = val;
}
val = getNumber(AMS_OBIS_POWER_FACTOR_L1, sizeof(AMS_OBIS_POWER_FACTOR_L1), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
l1PowerFactor = val;
}
val = getNumber(AMS_OBIS_POWER_FACTOR_L2, sizeof(AMS_OBIS_POWER_FACTOR_L2), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
l2PowerFactor = val;
}
val = getNumber(AMS_OBIS_POWER_FACTOR_L3, sizeof(AMS_OBIS_POWER_FACTOR_L3), ((char *) (d)));
if(val != 0xFFFFFFFF) {
if(val != NOVALUE) {
l3PowerFactor = val;
}
@@ -335,14 +335,15 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, uint8_t distribution
}
threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0;
twoPhase = (l1voltage > 0 && l2voltage > 0) || (l2voltage > 0 && l3voltage > 0) || (l3voltage > 0 && l1voltage > 0);
if(!threePhase)
twoPhase = (l1voltage > 0 && l2voltage > 0) || (l2voltage > 0 && l3voltage > 0) || (l3voltage > 0 && l1voltage > 0);
// Special case for Norwegian IT/TT meters that does not report all values
if(distributionSystem == 1) {
if(threePhase) {
if(l2current == 0.0 && l1current > 0.0 && l3current > 0.0) {
l2current = (((activeImportPower - activeExportPower) * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage;
if(activeExportPower == 0) {
if(activeExportPower == 0.0) {
l2current = max((float) 0.0, l2current);
}
}
@@ -453,31 +454,38 @@ double IEC6205675::getNumber(uint8_t* obis, int matchlength, const char* ptr) {
}
double IEC6205675::getNumber(CosemData* item) {
double val = 0xFFFFFFFF;
if(item != NULL) {
double ret = 0.0;
char* pos = ((char*) item);
switch(item->base.type) {
case CosemTypeLongUnsigned:
val = ntohs(item->lu.data);
case CosemTypeLongUnsigned: {
uint16_t u16 = ntohs(item->lu.data);
ret = u16;
pos += 3;
break;
case CosemTypeDLongUnsigned:
val = ntohl(item->dlu.data);
}
case CosemTypeDLongUnsigned: {
uint32_t u32 = ntohl(item->dlu.data);
ret = u32;
pos += 5;
break;
case CosemTypeLongSigned:
val = ntohs(item->lu.data);
}
case CosemTypeLongSigned: {
int16_t i16 = ntohs(item->ls.data);
ret = i16;
pos += 3;
break;
}
}
if(pos != NULL) {
if(*pos++ == 0x02 && *pos++ == 0x02) {
int8_t scale = *++pos;
val *= pow(10, scale);
ret *= pow(10, scale);
}
}
return ret;
}
return val;
return NOVALUE;
}
time_t IEC6205675::getTimestamp(uint8_t* obis, int matchlength, const char* ptr) {

View File

@@ -4,6 +4,8 @@
#include "AmsData.h"
#include "ams/hdlc.h"
#define NOVALUE 0xFFFFFFFF
struct AmsOctetTimestamp {
uint8_t type;
CosemDateTime dt;

View File

@@ -1,4 +1,5 @@
#include "DnbCurrParser.h"
#include "Arduino.h"
#include "HardwareSerial.h"
float DnbCurrParser::getValue() {
@@ -29,13 +30,29 @@ size_t DnbCurrParser::write(const uint8_t *buffer, size_t size) {
}
size_t DnbCurrParser::write(uint8_t byte) {
if(pos >= 64) pos = 0;
if(pos == 0) {
if(byte == '<') {
buf[pos++] = byte;
}
} else if(byte == '>') {
buf[pos++] = byte;
if(strncmp(buf, "<Obs", 4) == 0) {
if(strncmp(buf, "<Series", 7) == 0) {
for(int i = 0; i < pos; i++) {
if(strncmp(buf+i, "UNIT_MULT=\"", 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;
}
}
scale = String(buf+pos).toInt();
} else if(strncmp(buf, "<Obs", 4) == 0) {
for(int i = 0; i < pos; i++) {
if(strncmp(buf+i, "OBS_VALUE=\"", 11) == 0) {
pos = i + 11;
@@ -49,7 +66,7 @@ size_t DnbCurrParser::write(uint8_t byte) {
break;
}
}
value = String(buf+pos).toFloat();
value = String(buf+pos).toFloat() / pow(10, scale);
}
pos = 0;
} else {

View File

@@ -15,6 +15,7 @@ public:
size_t write(uint8_t);
private:
uint8_t scale = 0;
float value = 1.0;
char buf[64];

View File

@@ -14,6 +14,7 @@ char* EntsoeA44Parser::getMeasurementUnit() {
}
float EntsoeA44Parser::getPoint(uint8_t position) {
if(position >= 24) return ENTSOE_NO_VALUE;
return points[position];
}
@@ -41,6 +42,7 @@ size_t EntsoeA44Parser::write(const uint8_t *buffer, size_t size) {
}
size_t EntsoeA44Parser::write(uint8_t byte) {
if(pos >= 64) pos = 0;
if(docPos == DOCPOS_CURRENCY) {
buf[pos++] = byte;
if(pos == 3) {

View File

@@ -4,21 +4,23 @@
#include "TimeLib.h"
#include "DnbCurrParser.h"
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
#include <HTTPClient.h>
#else
#warning "Unsupported board type"
#if defined(ESP32)
#include <esp_task_wdt.h>
#endif
EntsoeApi::EntsoeApi(RemoteDebug* Debug) {
debugger = Debug;
client.setInsecure();
https.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
https.setTimeout(20000);
// 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);
tomorrowFetchMillis = 36000000 + (random(1800) * 1000); // Random between 13:30 and 14:00
}
void EntsoeApi::setup(EntsoeConfig& config) {
@@ -26,6 +28,7 @@ void EntsoeApi::setup(EntsoeConfig& config) {
this->config = new EntsoeConfig();
}
memcpy(this->config, &config, sizeof(config));
lastCurrencyFetch = 0;
}
char* EntsoeApi::getToken() {
@@ -61,8 +64,10 @@ float EntsoeApi::getValueForHour(time_t cur, uint8_t hour) {
} else {
return ENTSOE_NO_VALUE;
}
multiplier *= getCurrencyMultiplier(tomorrow->getCurrency(), config->currency);
} else {
float mult = getCurrencyMultiplier(tomorrow->getCurrency(), config->currency);
if(mult == 0) return ENTSOE_NO_VALUE;
multiplier *= mult;
} else if(pos >= 0) {
if(today == NULL)
return ENTSOE_NO_VALUE;
value = today->getPoint(pos);
@@ -71,7 +76,9 @@ float EntsoeApi::getValueForHour(time_t cur, uint8_t hour) {
} else {
return ENTSOE_NO_VALUE;
}
multiplier *= getCurrencyMultiplier(today->getCurrency(), config->currency);
float mult = getCurrencyMultiplier(today->getCurrency(), config->currency);
if(mult == 0) return ENTSOE_NO_VALUE;
multiplier *= mult;
}
return value * multiplier;
}
@@ -79,7 +86,6 @@ float EntsoeApi::getValueForHour(time_t cur, uint8_t hour) {
bool EntsoeApi::loop() {
if(strlen(getToken()) == 0)
return false;
bool ret = false;
uint64_t now = millis64();
if(now < 10000) return false; // Grace period
@@ -121,21 +127,31 @@ bool EntsoeApi::loop() {
d2.Year+1970, d2.Month, d2.Day, 23, 00,
config->area, config->area);
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Fetching prices for today\n");
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", url);
EntsoeA44Parser* a44 = new EntsoeA44Parser();
if(retrieve(url, a44) && a44->getPoint(0) != ENTSOE_NO_VALUE) {
today = a44;
ret = true;
return true;
} else if(a44 != NULL) {
delete a44;
today = NULL;
return false;
}
}
// Prices for next day are published at 13:00 CE(S)T, but to avoid heavy server traffic at that time, we will
// fetch 1 hr after that (with some random delay) and retry every 15 minutes
if(tomorrow == NULL
&& midnightMillis - now < 39600000 // Fetch 11hrs before midnight (13:00 CE(S)T)
&& (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 300000) // Retry every 5min
&& midnightMillis - now < tomorrowFetchMillis
&& (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 900000)
) {
lastTomorrowFetch = now;
time_t e1 = time(nullptr);
@@ -151,23 +167,29 @@ bool EntsoeApi::loop() {
d2.Year+1970, d2.Month, d2.Day, 23, 00,
config->area, config->area);
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Fetching prices for tomorrow\n");
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", url);
EntsoeA44Parser* a44 = new EntsoeA44Parser();
if(retrieve(url, a44) && a44->getPoint(0) != ENTSOE_NO_VALUE) {
tomorrow = a44;
ret = true;
return true;
} else if(a44 != NULL) {
delete a44;
tomorrow = NULL;
return false;
}
}
}
return ret;
return false;
}
bool EntsoeApi::retrieve(const char* url, Stream* doc) {
WiFiClientSecure client;
#if defined(ESP8266)
// https://arduino-esp8266.readthedocs.io/en/latest/esp8266wifi/bearssl-client-secure-class.html#mfln-or-maximum-fragment-length-negotiation-saving-ram
/* Rumor has it that a client cannot request a lower max_fragment_length, so I guess thats why the following does not work.
@@ -185,13 +207,15 @@ bool EntsoeApi::retrieve(const char* url, Stream* doc) {
*/
#endif
client.setInsecure();
HTTPClient https;
https.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
if(https.begin(client, url)) {
printD("Connection established");
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
/*
#if defined(ESP8266)
if(!client.getMFLNStatus()) {
@@ -204,6 +228,13 @@ bool EntsoeApi::retrieve(const char* url, Stream* doc) {
*/
int status = https.GET();
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
if(status == HTTP_CODE_OK) {
printD("Receiving data");
https.writeToStream(doc);
@@ -239,18 +270,40 @@ float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to) {
return 1.00;
uint64_t now = millis64();
if(lastCurrencyFetch == 0 || now - lastCurrencyFetch > (SECS_PER_HOUR * 1000)) {
char url[256];
snprintf(url, sizeof(url), "https://data.norges-bank.no/api/data/EXR/M.%s.%s.SP?lastNObservations=1",
from,
to
);
DnbCurrParser p;
if(retrieve(url, &p)) {
currencyMultiplier = p.getValue();
}
if(now > lastCurrencyFetch && (now - lastCurrencyFetch) < 900000) {
lastCurrencyFetch = now;
char url[256];
DnbCurrParser p;
#if defined(ESP32)
esp_task_wdt_reset();
#elif defined(ESP8266)
ESP.wdtFeed();
#endif
snprintf(url, sizeof(url), "https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1", from);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Retrieving %s to NOK conversion\n", from);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", url);
if(retrieve(url, &p)) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue());
currencyMultiplier = p.getValue();
if(strncmp(to, "NOK", 3) != 0) {
snprintf(url, sizeof(url), "https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1", to);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Retrieving %s to NOK conversion\n", to);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", url);
if(retrieve(url, &p)) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue());
currencyMultiplier /= p.getValue();
} else {
return 0;
}
}
} else {
return 0;
}
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) Resulting currency multiplier: %.4f\n", currencyMultiplier);
lastCurrencyFetch = midnightMillis;
}
return currencyMultiplier;
}

View File

@@ -7,6 +7,14 @@
#include "EntsoeA44Parser.h"
#include "AmsConfiguration.h"
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
#include <HTTPClient.h>
#else
#warning "Unsupported board type"
#endif
#define ENTSOE_DEFAULT_MULTIPLIER 1.00
#define SSL_BUF_SIZE 512
@@ -24,7 +32,10 @@ public:
private:
RemoteDebug* debugger;
EntsoeConfig* config = NULL;
WiFiClientSecure client;
HTTPClient https;
uint32_t tomorrowFetchMillis = 36000000; // Number of ms before midnight. Default fetch 10hrs before midnight (14:00 CE(S)T)
uint64_t midnightMillis = 0;
uint64_t lastTodayFetch = 0;
uint64_t lastTomorrowFetch = 0;

View File

@@ -14,6 +14,10 @@
#define HEAD_HTML_LEN HEAD32_HTML_LEN
#endif
#if defined(ESP32)
#include <esp_task_wdt.h>
#endif
#include "root/foot_html.h"
#include "root/index_html.h"
#include "root/application_js.h"
@@ -322,9 +326,6 @@ void AmsWebServer::indexHtml() {
for(int i = 0; i<255; i++) {
html.replace("${config.boardType" + String(i) + "}", sys.boardType == i ? "selected" : "");
}
for(int i = 0; i<5; i++) {
html.replace("${config.meterType" + String(i) + "}", sys.boardType == i ? "selected" : "");
}
html.replace("${config.wifiSsid}", wifi.ssid);
html.replace("${config.wifiPassword}", wifi.psk);
html.replace("${config.wifiStaticIp}", strlen(wifi.ip) > 0 ? "checked" : "");
@@ -457,9 +458,6 @@ void AmsWebServer::configMeterHtml() {
html.replace("{d" + String(i) + "}", meterConfig->distributionSystem == i ? "selected" : "");
}
html.replace("{f}", String(meterConfig->mainFuse));
for(int i = 0; i<64; i++) {
html.replace("{f" + String(i) + "}", meterConfig->mainFuse == i ? "selected" : "");
}
html.replace("{p}", String(meterConfig->productionCapacity));
if(meterConfig->encryptionKey[0] != 0x00) {
@@ -497,12 +495,21 @@ void AmsWebServer::configWifiHtml() {
html.replace("{s}", wifi.ssid);
html.replace("{p}", wifi.psk);
html.replace("{st}", strlen(wifi.ip) > 0 ? "checked" : "");
html.replace("{i}", wifi.ip);
html.replace("{g}", wifi.gateway);
html.replace("{sn}", wifi.subnet);
html.replace("{d1}", wifi.dns1);
html.replace("{d2}", wifi.dns2);
if(strlen(wifi.ip) > 0) {
html.replace("{st}", "checked");
html.replace("{i}", wifi.ip);
html.replace("{g}", wifi.gateway);
html.replace("{sn}", wifi.subnet);
html.replace("{d1}", wifi.dns1);
html.replace("{d2}", wifi.dns2);
} else {
html.replace("{st}", "");
html.replace("{i}", WiFi.localIP().toString());
html.replace("{g}", WiFi.gatewayIP().toString());
html.replace("{sn}", WiFi.subnetMask().toString());
html.replace("{d1}", WiFi.dnsIP().toString());
html.replace("{d2}", "");
}
html.replace("{h}", wifi.hostname);
html.replace("{m}", wifi.mdns ? "checked" : "");
@@ -747,7 +754,7 @@ void AmsWebServer::dataJson() {
if(eapi != NULL && strlen(eapi->getToken()) > 0)
price = eapi->getValueForHour(0);
char json[384];
char json[512];
snprintf_P(json, sizeof(json), DATA_JSON,
maxPwr == 0 ? meterState->isThreePhase() ? 20000 : 10000 : maxPwr,
meterConfig->productionCapacity,
@@ -781,8 +788,9 @@ void AmsWebServer::dataJson() {
mqttStatus,
mqtt == NULL ? 0 : (int) mqtt->lastError(),
price == ENTSOE_NO_VALUE ? "null" : String(price, 2).c_str(),
time(nullptr),
meterState->getMeterType()
meterState->getMeterType(),
meterConfig->distributionSystem,
(uint32_t) time(nullptr)
);
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
@@ -1029,7 +1037,6 @@ void AmsWebServer::handleSetup() {
break;
case 200: // ESP32
gpioConfig->hanPin = 16;
gpioConfig->apPin = 0;
gpioConfig->ledPin = 2;
gpioConfig->ledInverted = false;
break;
@@ -1117,6 +1124,7 @@ void AmsWebServer::handleSave() {
meterConfig->distributionSystem = server.arg("d").toInt();
meterConfig->mainFuse = server.arg("f").toInt();
meterConfig->productionCapacity = server.arg("p").toInt();
maxPwr = 0;
String encryptionKeyHex = server.arg("e");
if(!encryptionKeyHex.isEmpty()) {
@@ -1561,6 +1569,13 @@ void AmsWebServer::firmwareUpload() {
String filename = upload.filename;
if(!filename.endsWith(".bin")) {
server.send(500, "text/plain", "500: couldn't create file");
} else {
#if defined(ESP32)
esp_task_wdt_delete(NULL);
esp_task_wdt_deinit();
#elif defined(ESP8266)
ESP.wdtDisable();
#endif
}
}
uploadFile(FILE_FIRMWARE);
@@ -1584,6 +1599,7 @@ void AmsWebServer::firmwareDownload() {
printI("Downloading firmware...");
HTTPClient httpClient;
httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
httpClient.setTimeout(20000);
httpClient.addHeader("User-Agent", "ams2mqtt/" + String(VERSION));
#if defined(ESP8266)
@@ -1602,6 +1618,13 @@ void AmsWebServer::firmwareDownload() {
if(status == HTTP_CODE_OK) {
printD("Received OK from server");
if(LittleFS.begin()) {
#if defined(ESP32)
esp_task_wdt_delete(NULL);
esp_task_wdt_deinit();
#elif defined(ESP8266)
ESP.wdtDisable();
#endif
printI("Downloading firmware to LittleFS");
file = LittleFS.open(FILE_FIRMWARE, "w");
int len = httpClient.writeToStream(&file);

View File

@@ -1,5 +1,6 @@
var nextVersion;
var im, em;
var ds = 0;
// Price plot
var pp;
@@ -231,9 +232,9 @@ $(function() {
// For wifi
$('#st').on('change', function() {
if($(this).is(':checked')) {
$('#i').show();
$('.sip').prop('disabled', false);
} else {
$('#i').hide();
$('.sip').prop('disabled', true);
}
});
$('#st').trigger('change');
@@ -597,12 +598,6 @@ var fetch = function() {
dataType: 'json',
}).done(function(json) {
retrycount = 0;
if(im) {
$(".SimpleMeter").hide();
im.show();
em.show();
}
for(var id in json) {
var str = json[id];
@@ -622,6 +617,8 @@ var fetch = function() {
$('.ju').html(moment.duration(parseInt(json.u), 'seconds').humanize());
}
ds = parseInt(json.ds);
var kib = parseInt(json.m)/1000;
$('.jm').html(kib.toFixed(1));
if(kib > 32) {
@@ -677,6 +674,14 @@ var fetch = function() {
}
if(vp) {
switch(ds) {
case 1:
vo.title = 'Voltage between';
break;
case 2:
vo.title = 'Phase voltage';
break;
}
var c = 0;
var t = 0;
var r = 1;
@@ -686,21 +691,21 @@ var fetch = function() {
t += u1;
c++;
var pct = (Math.max(parseFloat(json.u1)-195.5, 1)*100/69);
arr[r++] = ['L1', u1, "color: " + voltcol(pct) + ";opacity: 0.9;", u1 + "V"];
arr[r++] = [ds == 1 ? 'L1-L2' : 'L1', u1, "color: " + voltcol(pct) + ";opacity: 0.9;", u1 + "V"];
}
if(json.u2) {
var u2 = parseFloat(json.u2);
t += u2;
c++;
var pct = (Math.max(parseFloat(json.u2)-195.5, 1)*100/69);
arr[r++] = ['L2', u2, "color: " + voltcol(pct) + ";opacity: 0.9;", u2 + "V"];
arr[r++] = [ds == 1 ? 'L1-L3' : 'L2', u2, "color: " + voltcol(pct) + ";opacity: 0.9;", u2 + "V"];
}
if(json.u3) {
var u3 = parseFloat(json.u3);
t += u3;
c++;
var pct = (Math.max(parseFloat(json.u3)-195.5, 1)*100/69);
arr[r++] = ['L3', u3, "color: " + voltcol(pct) + ";opacity: 0.9;", u3 + "V"];
arr[r++] = [ds == 1 ? 'L2-L3' : 'L3', u3, "color: " + voltcol(pct) + ";opacity: 0.9;", u3 + "V"];
}
v = t/c;
if(v > 0) {
@@ -710,6 +715,14 @@ var fetch = function() {
}
if(ap && json.mf) {
switch(ds) {
case 1:
ao.title = 'Line current';
break;
case 2:
ao.title = 'Phase current';
break;
}
var a = 0;
var r = 1;
var arr = [['Phase', 'Amperage', { role: 'style' }, { role: 'annotation' }]];

View File

@@ -31,6 +31,7 @@
"mm" : %d,
"me" : %d,
"p" : %s,
"c" : %lu,
"mt" : %d
"mt" : %d,
"ds" : %d,
"c" : %lu
}

View File

@@ -61,7 +61,7 @@
</div>
</div>
<div class="row">
<div class="col-xl-3 col-lg-4 col-md-5 col-sm-6">
<div class="col-xl-3 col-lg-4 col-md-5 col-sm-7">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Distribution system</span>
@@ -73,22 +73,15 @@
</select>
</div>
</div>
<div class="col-xl-2 col-md-3 col-sm-5">
<div class="col-lg-3 col-md-4 col-sm-5">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">Main fuse</span>
</div>
<select class="form-control" name="f">
<option value="0" {f0}></option>
<option value="16" {f16}>16A</option>
<option value="20" {f20}>20A</option>
<option value="25" {f25}>25A</option>
<option value="32" {f32}>32A</option>
<option value="35" {f35}>35A</option>
<option value="40" {f40}>40A</option>
<option value="50" {f50}>50A</option>
<option value="63" {f63}>63A</option>
</select>
<input class="form-control text-right" name="f" type="number" min="5" max="255" step="1" value="{f}"/>
<div class="input-group-append">
<span class="input-group-text">A</span>
</div>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-md-5 col-sm-7">
@@ -96,7 +89,7 @@
<div class="input-group-prepend">
<span class="input-group-text">Production capacity</span>
</div>
<input class="form-control" name="p" type="number" min="0" max="50" value="{p}"/>
<input class="form-control text-right" name="p" type="number" min="0" max="255" step="1" value="{p}"/>
<div class="input-group-append">
<span class="input-group-text">kWp</span>
</div>

View File

@@ -42,7 +42,7 @@
<div class="input-group-prepend">
<span class="input-group-text">IP</span>
</div>
<input type="text" name="i" class="form-control" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{i}"/>
<input type="text" name="i" class="form-control sip" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{i}"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 form-group">
@@ -50,7 +50,7 @@
<div class="input-group-prepend">
<span class="input-group-text">Subnet</span>
</div>
<input type="text" name="sn" class="form-control" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{sn}"/>
<input type="text" name="sn" class="form-control sip" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{sn}"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 form-group">
@@ -58,7 +58,7 @@
<div class="input-group-prepend">
<span class="input-group-text">Gateway</span>
</div>
<input type="text" name="g" class="form-control" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{g}"/>
<input type="text" name="g" class="form-control sip" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{g}"/>
</div>
</div>
<div class="col-xl-4 col-lg-5 form-group">
@@ -66,7 +66,7 @@
<div class="input-group-prepend">
<span class="input-group-text">DNS 1</span>
</div>
<input type="text" name="d1" class="form-control" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{d1}"/>
<input type="text" name="d1" class="form-control sip" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{d1}"/>
</div>
</div>
<div class="col-xl-4 col-lg-5 form-group">
@@ -74,7 +74,7 @@
<div class="input-group-prepend">
<span class="input-group-text">DNS 2</span>
</div>
<input type="text" name="d2" class="form-control" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{d2}"/>
<input type="text" name="d2" class="form-control sip" pattern="\d?\d?\d.\d?\d?\d.\d?\d?\d.\d?\d?\d" value="{d2}"/>
</div>
</div>
</div>

BIN
webui2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB