Energy accounting

This commit is contained in:
Gunnar Skjold 2022-01-29 19:27:48 +01:00
parent 8c69f9a738
commit 77ce5d8e90
25 changed files with 483 additions and 342 deletions

View File

@ -492,6 +492,63 @@ void AmsConfiguration::ackEntsoeChange() {
entsoeChanged = false;
}
bool AmsConfiguration::getEnergyAccountingConfig(EnergyAccountingConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_ENERGYACCOUNTING_START, config);
EEPROM.end();
if(config.thresholds[9] != 255) {
clearEnergyAccountingConfig(config);
}
return true;
} else {
return false;
}
}
bool AmsConfiguration::setEnergyAccountingConfig(EnergyAccountingConfig& config) {
EnergyAccountingConfig existing;
if(getEnergyAccountingConfig(existing)) {
for(int i = 0; i < 9; i++) {
if(existing.thresholds[i] != config.thresholds[i]) {
energyAccountingChanged = true;
}
}
config.thresholds[9] = 255;
} else {
energyAccountingChanged = true;
}
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_ENERGYACCOUNTING_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
void AmsConfiguration::clearEnergyAccountingConfig(EnergyAccountingConfig& config) {
config.thresholds[0] = 5;
config.thresholds[1] = 10;
config.thresholds[2] = 15;
config.thresholds[3] = 20;
config.thresholds[4] = 25;
config.thresholds[5] = 50;
config.thresholds[6] = 75;
config.thresholds[7] = 100;
config.thresholds[8] = 150;
config.thresholds[9] = 255;
}
bool AmsConfiguration::isEnergyAccountingChanged() {
return energyAccountingChanged;
}
void AmsConfiguration::ackEnergyAccountingChange() {
energyAccountingChanged = false;
}
void AmsConfiguration::clear() {
EEPROM.begin(EEPROM_SIZE);
MeterConfig meter;
@ -522,6 +579,10 @@ void AmsConfiguration::clear() {
clearEntsoe(entsoe);
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
EnergyAccountingConfig eac;
clearEnergyAccountingConfig(eac);
EEPROM.put(CONFIG_ENERGYACCOUNTING_START, eac);
EEPROM.put(EEPROM_CONFIG_ADDRESS, -1);
EEPROM.commit();
EEPROM.end();
@ -534,16 +595,6 @@ bool AmsConfiguration::hasConfig() {
EEPROM.end();
}
switch(configVersion) {
case 83:
configVersion = -1; // Prevent loop
if(loadConfig83(EEPROM_CONFIG_ADDRESS+1)) {
configVersion = EEPROM_CHECK_SUM;
return true;
} else {
configVersion = 0;
return false;
}
break;
case 86:
configVersion = -1; // Prevent loop
if(relocateConfig86()) {
@ -627,116 +678,6 @@ void AmsConfiguration::saveTempSensors() {
}
}
bool AmsConfiguration::loadConfig83(int address) {
ConfigObject83 c;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(address, c);
EntsoeConfig entsoe {"", "", "", 1000};
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
NtpConfig ntp {
c.ntpEnable,
c.ntpDhcp,
c.ntpOffset,
c.ntpSummerOffset
};
strcpy(ntp.server, c.ntpServer);
EEPROM.put(CONFIG_NTP_START, ntp);
DomoticzConfig domo {
c.domoELIDX,
c.domoVL1IDX,
c.domoVL2IDX,
c.domoVL3IDX,
c.domoCL1IDX
};
EEPROM.put(CONFIG_DOMOTICZ_START, domo);
GpioConfig gpio {
c.hanPin,
c.apPin,
c.ledPin,
c.ledInverted,
c.ledPinRed,
c.ledPinGreen,
c.ledPinBlue,
c.ledRgbInverted,
c.tempSensorPin,
c.tempAnalogSensorPin,
c.vccPin,
c.vccOffset,
c.vccMultiplier,
c.vccBootLimit,
0,
0
};
EEPROM.put(CONFIG_GPIO_START, gpio);
DebugConfig debug {
c.debugTelnet,
c.debugSerial,
c.debugLevel
};
EEPROM.put(CONFIG_DEBUG_START, debug);
MeterConfig meter {
2400,
c.meterType == 3 || c.meterType == 4 ? 3 : 11,
false,
c.distributionSystem,
c.mainFuse,
c.productionCapacity,
{0},
{0}
};
memcpy(meter.encryptionKey, c.meterEncryptionKey, 16);
memcpy(meter.authenticationKey, c.meterAuthenticationKey, 16);
EEPROM.put(CONFIG_METER_START, meter);
WebConfig web {
c.authSecurity
};
strcpy(web.username, c.authUser);
strcpy(web.password, c.authPassword);
EEPROM.put(CONFIG_WEB_START, web);
MqttConfig mqtt;
strcpy(mqtt.host, c.mqttHost);
mqtt.port = c.mqttPort;
strcpy(mqtt.clientId, c.mqttClientId);
strcpy(mqtt.publishTopic, c.mqttPublishTopic);
strcpy(mqtt.subscribeTopic, c.mqttSubscribeTopic);
strcpy(mqtt.username, c.mqttUser);
strcpy(mqtt.password, c.mqttPassword);
mqtt.payloadFormat = c.mqttPayloadFormat;
mqtt.ssl = c.mqttSsl;
EEPROM.put(CONFIG_MQTT_START, mqtt);
WiFiConfig wifi;
strcpy(wifi.ssid, c.wifiSsid);
strcpy(wifi.psk, c.wifiPassword);
strcpy(wifi.ip, c.wifiIp);
strcpy(wifi.gateway, c.wifiGw);
strcpy(wifi.subnet, c.wifiSubnet);
strcpy(wifi.dns1, c.wifiDns1);
strcpy(wifi.dns2, c.wifiDns2);
strcpy(wifi.hostname, c.wifiHostname);
wifi.mdns = c.mDnsEnable;
EEPROM.put(CONFIG_WIFI_START, wifi);
SystemConfig sys {
c.boardType
};
EEPROM.put(CONFIG_SYSTEM_START, sys);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
bool AmsConfiguration::relocateConfig86() {
MqttConfig86 mqtt86;
MqttConfig mqtt;

View File

@ -13,6 +13,7 @@
#define CONFIG_GPIO_START 266
#define CONFIG_ENTSOE_START 290
#define CONFIG_WIFI_START 360
#define CONFIG_ENERGYACCOUNTING_START 520
#define CONFIG_WEB_START 648
#define CONFIG_DEBUG_START 824
#define CONFIG_DOMOTICZ_START 856
@ -152,71 +153,9 @@ struct EntsoeConfig {
uint32_t multiplier;
}; // 62
struct ConfigObject83 {
uint8_t boardType;
char wifiSsid[32];
char wifiPassword[64];
char wifiIp[15];
char wifiGw[15];
char wifiSubnet[15];
char wifiDns1[15];
char wifiDns2[15];
char wifiHostname[32];
char mqttHost[128];
uint16_t mqttPort;
char mqttClientId[32];
char mqttPublishTopic[64];
char mqttSubscribeTopic[64];
char mqttUser[64];
char mqttPassword[64];
uint8_t mqttPayloadFormat;
bool mqttSsl;
uint8_t authSecurity;
char authUser[64];
char authPassword[64];
uint8_t meterType;
uint8_t distributionSystem;
uint8_t mainFuse;
uint8_t productionCapacity;
uint8_t meterEncryptionKey[16];
uint8_t meterAuthenticationKey[16];
bool substituteMissing;
bool sendUnknown;
bool debugTelnet;
bool debugSerial;
uint8_t debugLevel;
uint8_t hanPin;
uint8_t apPin;
uint8_t ledPin;
bool ledInverted;
uint8_t ledPinRed;
uint8_t ledPinGreen;
uint8_t ledPinBlue;
bool ledRgbInverted;
uint8_t tempSensorPin;
uint8_t vccPin;
int16_t vccOffset;
uint16_t vccMultiplier;
uint8_t vccBootLimit;
uint16_t domoELIDX;
uint16_t domoVL1IDX;
uint16_t domoVL2IDX;
uint16_t domoVL3IDX;
uint16_t domoCL1IDX;
bool mDnsEnable;
bool ntpEnable;
bool ntpDhcp;
int16_t ntpOffset;
int16_t ntpSummerOffset;
char ntpServer[64];
uint8_t tempAnalogSensorPin;
};
struct EnergyAccountingConfig {
uint8_t thresholds[10];
}; // 10
struct TempSensorConfig {
uint8_t address[8];
@ -288,6 +227,12 @@ public:
bool isEntsoeChanged();
void ackEntsoeChange();
bool getEnergyAccountingConfig(EnergyAccountingConfig&);
bool setEnergyAccountingConfig(EnergyAccountingConfig&);
void clearEnergyAccountingConfig(EnergyAccountingConfig&);
bool isEnergyAccountingChanged();
void ackEnergyAccountingChange();
void loadTempSensors();
void saveTempSensors();
uint8_t getTempSensorCount();
@ -303,14 +248,13 @@ protected:
private:
uint8_t configVersion = 0;
bool wifiChanged, mqttChanged, meterChanged = true, domoChanged, ntpChanged = true, entsoeChanged = false;
bool wifiChanged, mqttChanged, meterChanged = true, domoChanged, ntpChanged = true, entsoeChanged = false, energyAccountingChanged = true;
uint8_t tempSensorCount = 0;
TempSensorConfig** tempSensors = NULL;
bool loadConfig83(int address);
bool relocateConfig86();
bool relocateConfig87();
bool relocateConfig86(); // 1.5.0
bool relocateConfig87(); // 1.5.4
bool relocateConfig90(); // 2.0.0
bool relocateConfig91(); // 2.0.2

View File

@ -1,6 +1,5 @@
#include "AmsDataStorage.h"
#include <lwip/apps/sntp.h>
#include "EEPROM.h"
#include "LittleFS.h"
#include "AmsStorage.h"
@ -16,8 +15,10 @@ void AmsDataStorage::setTimezone(Timezone* tz) {
bool AmsDataStorage::update(AmsData* data) {
time_t now = time(nullptr);
if(debugger->isActive(RemoteDebug::VERBOSE)) {
debugger->printf("(AmsDataStorage) Time is: %d\n", now);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Time is: %d\n", now);
if(tz == NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(AmsDataStorage) Timezone is missing\n");
return false;
}
if(now < EPOCH_2021_01_01) {
if(data->getMeterTimestamp() > 0) {
@ -38,22 +39,26 @@ bool AmsDataStorage::update(AmsData* data) {
}
return false;
}
if(now-day.lastMeterReadTime < 3595) {
if(now-day.lastMeterReadTime < 3500) {
if(debugger->isActive(RemoteDebug::VERBOSE)) {
debugger->printf("(AmsDataStorage) It is only %d seconds since last update, ignoring\n", (now-day.lastMeterReadTime));
}
return false;
}
tmElements_t tm, last;
breakTime(now, tm);
tmElements_t utc, ltz, utcYesterday, ltzYesterDay;
breakTime(now, utc);
breakTime(tz->toLocal(now), ltz);
breakTime(now-3600, utcYesterday);
breakTime(tz->toLocal(now-3600), ltzYesterDay);
if(day.lastMeterReadTime > EPOCH_2021_01_01) {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Last day update: %d\n", day.lastMeterReadTime);
}
tmElements_t last;
breakTime(day.lastMeterReadTime, last);
for(int i = last.Hour; i < tm.Hour; i++) {
for(int i = last.Hour; i < utc.Hour; i++) {
if(debugger->isActive(RemoteDebug::VERBOSE)) {
debugger->printf("(AmsDataStorage) Clearing hour: %d\n", i);
}
@ -65,15 +70,9 @@ bool AmsDataStorage::update(AmsData* data) {
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Last month update: %d\n", month.lastMeterReadTime);
}
if(tz != NULL) {
breakTime(tz->toLocal(now), tm);
breakTime(tz->toLocal(month.lastMeterReadTime), last);
} else {
breakTime(now, tm);
breakTime(month.lastMeterReadTime, last);
}
for(int i = last.Day; i < tm.Day; i++) {
tmElements_t last;
breakTime(tz->toLocal(month.lastMeterReadTime), last);
for(int i = last.Day; i < ltz.Day; i++) {
if(debugger->isActive(RemoteDebug::VERBOSE)) {
debugger->printf("(AmsDataStorage) Clearing day: %d\n", i);
}
@ -88,10 +87,11 @@ bool AmsDataStorage::update(AmsData* data) {
day.activeImport = data->getActiveImportCounter() * 1000;
day.activeExport = data->getActiveExportCounter() * 1000;
day.lastMeterReadTime = now;
return true;
}
if(data->getListType() != 3) return false;
else if(tm.Minute > 5) return false;
else if(ltz.Minute > 1) return false;
// Update day plot
if(day.activeImport == 0 || now - day.lastMeterReadTime > 86400) {
@ -105,13 +105,10 @@ bool AmsDataStorage::update(AmsData* data) {
setHour(i, 0);
}
} else if(now - day.lastMeterReadTime < 4000) {
breakTime(now - 3600, tm);
int16_t val = (((data->getActiveImportCounter() * 1000) - day.activeImport) - ((data->getActiveExportCounter() * 1000) - day.activeExport));
setHour(tm.Hour, val);
setHour(utcYesterday.Hour, val);
if(debugger->isActive(RemoteDebug::INFO)) {
debugger->printf("(AmsDataStorage) Usage for hour %d: %d\n", tm.Hour, val);
}
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(AmsDataStorage) Usage for hour %d: %d\n", ltzYesterDay.Hour, val);
day.activeImport = data->getActiveImportCounter() * 1000;
day.activeExport = data->getActiveExportCounter() * 1000;
@ -127,10 +124,10 @@ bool AmsDataStorage::update(AmsData* data) {
debugger->printf("(AmsDataStorage) Since last day update, minutes: %.1f, import: %d (%.2f/min), export: %d (%.2f/min)\n", mins, im, ipm, ex, epm);
}
breakTime(day.lastMeterReadTime, tm);
day.lastMeterReadTime = day.lastMeterReadTime - (tm.Minute * 60) - tm.Second;
breakTime(now, tm);
time_t stopAt = now - (tm.Minute * 60) - tm.Second;
tmElements_t last;
breakTime(day.lastMeterReadTime, last);
day.lastMeterReadTime = day.lastMeterReadTime - (last.Minute * 60) - last.Second;
time_t stopAt = now - (utc.Minute * 60) - utc.Second;
while(day.lastMeterReadTime < stopAt) {
time_t cur = min(day.lastMeterReadTime + 3600, stopAt);
uint8_t minutes = round((cur - day.lastMeterReadTime) / 60.0);
@ -151,12 +148,6 @@ bool AmsDataStorage::update(AmsData* data) {
}
// Update month plot
if(tz != NULL) {
breakTime(tz->toLocal(now), tm);
} else {
breakTime(now, tm);
}
if(month.lastMeterReadTime > now) {
if(debugger->isActive(RemoteDebug::WARNING)) {
debugger->printf("(AmsDataStorage) Invalid future timestamp for month plot, resetting\n");
@ -164,9 +155,7 @@ bool AmsDataStorage::update(AmsData* data) {
month.activeImport = data->getActiveImportCounter() * 1000;
month.activeExport = data->getActiveExportCounter() * 1000;
month.lastMeterReadTime = now;
}
if(tm.Hour == 0 && now - month.lastMeterReadTime > 86300) {
} else if(ltz.Hour == 0) {
if(month.activeImport == 0 || now - month.lastMeterReadTime > 2678400) {
month.activeImport = data->getActiveImportCounter() * 1000;
month.activeExport = data->getActiveExportCounter() * 1000;
@ -177,17 +166,14 @@ bool AmsDataStorage::update(AmsData* data) {
for(int i = 1; i<=31; i++) {
setDay(i, 0);
}
} else if(now - month.lastMeterReadTime < 87000) {
} else if(now - month.lastMeterReadTime < 86500 && now - month.lastMeterReadTime > 86300) {
int32_t val = (month.activeImport == 0 ? 0 : ((data->getActiveImportCounter() * 1000) - month.activeImport) - ((data->getActiveExportCounter() * 1000) - month.activeExport));
if(debugger->isActive(RemoteDebug::INFO)) {
debugger->printf("(AmsDataStorage) Usage for day %d: %d\n", tm.Day, val);
debugger->printf("(AmsDataStorage) Usage for day %d: %d\n", ltzYesterDay.Day, val);
}
time_t yesterday = now - 3600;
breakTime(yesterday, tm);
setDay(tm.Day, val);
setDay(ltzYesterDay.Day, val);
month.activeImport = data->getActiveImportCounter() * 1000;
month.activeExport = data->getActiveExportCounter() * 1000;
month.lastMeterReadTime = now;
@ -206,32 +192,19 @@ bool AmsDataStorage::update(AmsData* data) {
}
// Make sure last month read is at midnight
if(tz != NULL) {
breakTime(tz->toLocal(month.lastMeterReadTime), tm);
} else {
breakTime(month.lastMeterReadTime, tm);
}
month.lastMeterReadTime = month.lastMeterReadTime - (tm.Hour * 3600) - (tm.Minute * 60) - tm.Second;
tmElements_t last;
breakTime(tz->toLocal(month.lastMeterReadTime), last);
month.lastMeterReadTime = month.lastMeterReadTime - (last.Hour * 3600) - (last.Minute * 60) - last.Second;
if(debugger->isActive(RemoteDebug::DEBUG)) {
debugger->printf("(AmsDataStorage) Last month read after resetting to midnight: %lu\n", month.lastMeterReadTime);
}
if(tz != NULL) {
breakTime(tz->toLocal(now), tm);
} else {
breakTime(now, tm);
}
time_t stopAt = now - (tm.Hour * 3600) - (tm.Minute * 60) - tm.Second;
time_t stopAt = now - (ltz.Hour * 3600) - (ltz.Minute * 60) - ltz.Second;
while(month.lastMeterReadTime < stopAt) {
time_t cur = min(month.lastMeterReadTime + 86400, stopAt);
uint8_t hours = round((cur - month.lastMeterReadTime) / 3600.0);
if(tz != NULL) {
breakTime(tz->toLocal(month.lastMeterReadTime), last);
} else {
breakTime(month.lastMeterReadTime, last);
}
breakTime(tz->toLocal(month.lastMeterReadTime), last);
float val = ((iph * hours) - (eph * hours));
setDay(last.Day, val);

View File

@ -9,5 +9,6 @@
#define FILE_DAYPLOT "/dayplot.bin"
#define FILE_MONTHPLOT "/monthplot.bin"
#define FILE_ENERGYACCOUNTING "/energyaccounting.bin"
#endif

View File

@ -303,6 +303,7 @@ void setup() {
tz = new Timezone(dst, std);
ws.setTimezone(tz);
ds.setTimezone(tz);
ea.setTimezone(tz);
}
ds.load();
@ -313,7 +314,13 @@ void setup() {
swapWifiMode();
}
ea.setup(&ds, eapi);
EnergyAccountingConfig *eac = new EnergyAccountingConfig();
if(!config.getEnergyAccountingConfig(*eac)) {
config.clearEnergyAccountingConfig(*eac);
config.setEnergyAccountingConfig(*eac);
config.ackEnergyAccountingChange();
}
ea.setup(&ds, eapi, eac);
ws.setup(&config, &gpioConfig, &meterConfig, &meterState, &ds, &ea);
#if defined(ESP32)
@ -420,6 +427,8 @@ void loop() {
TimeChangeRule dst = {"DST", Last, Sun, Mar, 2, (ntp.offset + ntp.summerOffset) / 6};
tz = new Timezone(dst, std);
ws.setTimezone(tz);
ds.setTimezone(tz);
ea.setTimezone(tz);
}
config.ackNtpChange();
@ -491,6 +500,13 @@ void loop() {
hc = NULL;
}
if(config.isEnergyAccountingChanged()) {
EnergyAccountingConfig *eac = ea.getConfig();
config.getEnergyAccountingConfig(*eac);
ea.setup(&ds, eapi, eac);
config.ackEnergyAccountingChange();
}
if(readHanPort() || now - meterState.getLastUpdateMillis() > 30000) {
if(now - lastTemperatureRead > 15000) {
unsigned long start = millis();
@ -842,7 +858,7 @@ bool readHanPort() {
if(!hw.ledBlink(LED_GREEN, 1))
hw.ledBlink(LED_INTERNAL, 1);
if(mqttEnabled && mqttHandler != NULL && mqtt != NULL) {
if(mqttHandler->publish(&data, &meterState)) {
if(mqttHandler->publish(&data, &meterState, &ea)) {
if(data.getListType() == 3 && eapi != NULL) {
mqttHandler->publishPrices(eapi);
}

View File

@ -1,94 +1,83 @@
#include "EnergyAccounting.h"
#include "LittleFS.h"
#include "AmsStorage.h"
EnergyAccounting::EnergyAccounting(RemoteDebug* debugger) {
data.version = 1;
this->debugger = debugger;
}
void EnergyAccounting::setup(AmsDataStorage *ds, EntsoeApi *eapi) {
void EnergyAccounting::setup(AmsDataStorage *ds, EntsoeApi *eapi, EnergyAccountingConfig *config) {
this->ds = ds;
this->eapi = eapi;
this->config = config;
this->currentThresholdIdx = 0;
}
EnergyAccountingConfig* EnergyAccounting::getConfig() {
return config;
}
void EnergyAccounting::setTimezone(Timezone* tz) {
this->tz = tz;
}
bool EnergyAccounting::update(AmsData* amsData) {
time_t now = time(nullptr);
if(now < EPOCH_2021_01_01) return false;
if(tz == NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) Timezone is missing\n");
return false;
}
bool ret = false;
tmElements_t tm;
breakTime(now, tm);
tmElements_t local;
breakTime(tz->toLocal(now), local);
if(!init) {
currentHour = local.Hour;
currentDay = local.Day;
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) Initializing data at %lu\n", now);
if(!load()) {
data = { 1, tm.Month, 0, 0, 0, 0 };
currentHour = tm.Hour;
currentDay = tm.Day;
for(int i = 0; i < tm.Hour; i++) {
int16_t val = ds->getHour(i) / 10.0;
if(val > data.maxHour) {
data.maxHour = val;
ret = true;
}
}
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) Unable to load existing data");
data = { 1, local.Month, 0, 0, 0, 0 };
if(calcDayUse()) ret = true;
}
init = true;
}
if(!initPrice && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) Initializing prices at %lu\n", now);
for(int i = 0; i < tm.Hour; i++) {
float price = eapi->getValueForHour(i-tm.Hour);
if(price == ENTSOE_NO_VALUE) break;
int16_t wh = ds->getHour(i);
double kwh = wh / 1000.0;
costDay += price * kwh;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" Hour: %d, wh: %d, kwh; %.2f, price: %.2f, costDay: %.4f\n", i, wh, kwh, price, costDay);
}
initPrice = true;
calcDayCost();
}
if(amsData->getListType() >= 3 && tm.Hour != currentHour) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New hour %d\n", tm.Hour);
if(tm.Hour > 0) {
if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
costDay = 0;
for(int i = 0; i < tm.Hour; i++) {
float price = eapi->getValueForHour(i-tm.Hour);
if(price == ENTSOE_NO_VALUE) break;
int16_t wh = ds->getHour(i);
costDay += price * (wh / 1000.0);
}
}
}
if(amsData->getListType() >= 3 && local.Hour != currentHour) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New local hour %d\n", local.Hour);
for(int i = 0; i < tm.Hour; i++) {
int16_t val = ds->getHour(i) / 10.0;
if(val > data.maxHour) {
data.maxHour = val;
ret = true;
}
if(calcDayUse()) ret = true;
if(local.Hour > 0) {
calcDayCost();
}
use = 0;
costHour = 0;
currentHour = tm.Hour;
currentHour = local.Hour;
if(tm.Day != currentDay) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New day %d\n", tm.Day);
if(local.Day != currentDay) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New day %d\n", local.Day);
data.costYesterday = costDay * 100;
data.costThisMonth += costDay * 100;
costDay = 0;
currentDay = tm.Day;
currentDay = local.Day;
ret = true;
}
if(tm.Month != data.month) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New month %d\n", tm.Month);
if(local.Month != data.month) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New month %d\n", local.Month);
data.costLastMonth = data.costThisMonth;
data.costThisMonth = 0;
data.maxHour = 0;
data.month = tm.Month;
data.month = local.Month;
currentThresholdIdx = 0;
ret = true;
}
@ -98,25 +87,62 @@ bool EnergyAccounting::update(AmsData* amsData) {
float kwh = (amsData->getActiveImportPower() * (((float) ms) / 3600000.0)) / 1000.0;
lastUpdateMillis = amsData->getLastUpdateMillis();
if(kwh > 0) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EnergyAccounting) Adding %.4f kWh\n", kwh);
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) Adding %.4f kWh\n", kwh);
use += kwh;
if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
float price = eapi->getValueForHour(0);
float cost = price * kwh;
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EnergyAccounting) and %.4f %s\n", cost / 100.0, eapi->getCurrency());
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) and %.4f %s\n", cost / 100.0, eapi->getCurrency());
costHour += cost;
costDay += cost;
}
}
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) calculating threshold, currently at %d\n", currentThresholdIdx);
while(getMonthMax() > thresholds[currentThresholdIdx] / 10.0 && currentThresholdIdx < 5) currentThresholdIdx++;
while(use > thresholds[currentThresholdIdx] / 10.0 && currentThresholdIdx < 5) currentThresholdIdx++;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) new threshold %d\n", currentThresholdIdx);
if(config != NULL) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) calculating threshold, currently at %d\n", currentThresholdIdx);
while(getMonthMax() > config->thresholds[currentThresholdIdx] && currentThresholdIdx < 10) currentThresholdIdx++;
while(use > config->thresholds[currentThresholdIdx] && currentThresholdIdx < 10) currentThresholdIdx++;
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) new threshold %d\n", currentThresholdIdx);
}
return ret;
}
bool EnergyAccounting::calcDayUse() {
time_t now = time(nullptr);
tmElements_t local, utc;
breakTime(tz->toLocal(now), local);
bool ret = false;
for(int i = 0; i < local.Hour; i++) {
breakTime(now - ((local.Hour - i) * 3600), utc);
int16_t val = ds->getHour(utc.Hour) / 10.0;
if(val > data.maxHour) {
data.maxHour = val;
ret = true;
}
}
return ret;
}
void EnergyAccounting::calcDayCost() {
time_t now = time(nullptr);
tmElements_t local, utc;
breakTime(tz->toLocal(now), local);
if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
if(!initPrice) costDay = 0;
for(int i = 0; i < local.Hour; i++) {
float price = eapi->getValueForHour(i - local.Hour);
if(price == ENTSOE_NO_VALUE) break;
breakTime(now - ((local.Hour - i) * 3600), utc);
int16_t wh = ds->getHour(utc.Hour);
costDay += price * (wh / 1000.0);
}
initPrice = true;
}
}
double EnergyAccounting::getUseThisHour() {
return use;
}
@ -129,10 +155,11 @@ double EnergyAccounting::getUseToday() {
float ret = 0.0;
time_t now = time(nullptr);
if(now < EPOCH_2021_01_01) return 0;
tmElements_t tm;
breakTime(now, tm);
for(int i = 0; i < tm.Hour; i++) {
ret += ds->getHour(i) / 1000.0;
tmElements_t local, utc;
breakTime(tz->toLocal(now), local);
for(int i = 0; i < local.Hour; i++) {
breakTime(now - ((local.Hour - i) * 3600), utc);
ret += ds->getHour(utc.Hour) / 1000.0;
}
return ret + getUseThisHour();
}
@ -149,7 +176,10 @@ double EnergyAccounting::getUseThisMonth() {
time_t now = time(nullptr);
if(now < EPOCH_2021_01_01) return 0;
tmElements_t tm;
breakTime(now, tm);
if(tz != NULL)
breakTime(tz->toLocal(now), tm);
else
breakTime(now, tm);
float ret = 0;
for(int i = 0; i < tm.Day; i++) {
ret += ds->getDay(i) / 1000.0;
@ -165,8 +195,10 @@ double EnergyAccounting::getCostLastMonth() {
return data.costLastMonth / 100.0;
}
float EnergyAccounting::getCurrentThreshold() {
return thresholds[currentThresholdIdx] / 10.0;
uint8_t EnergyAccounting::getCurrentThreshold() {
if(config == NULL)
return 0;
return config->thresholds[currentThresholdIdx];
}
float EnergyAccounting::getMonthMax() {
@ -174,9 +206,51 @@ float EnergyAccounting::getMonthMax() {
}
bool EnergyAccounting::load() {
return false; // TODO
if(!LittleFS.begin()) {
if(debugger->isActive(RemoteDebug::ERROR)) {
debugger->printf("(EnergyAccounting) Unable to load LittleFS\n");
}
return false;
}
bool ret = false;
if(LittleFS.exists(FILE_ENERGYACCOUNTING)) {
File file = LittleFS.open(FILE_ENERGYACCOUNTING, "r");
char buf[file.size()];
file.readBytes(buf, file.size());
EnergyAccountingData* data = (EnergyAccountingData*) buf;
file.close();
if(data->version == 1) {
memcpy(&this->data, data, sizeof(this->data));
ret = true;
} else {
ret = false;
}
}
LittleFS.end();
return ret;
}
bool EnergyAccounting::save() {
return false; // TODO
if(!LittleFS.begin()) {
if(debugger->isActive(RemoteDebug::ERROR)) {
debugger->printf("(EnergyAccounting) Unable to load LittleFS\n");
}
return false;
}
{
File file = LittleFS.open(FILE_ENERGYACCOUNTING, "w");
char buf[sizeof(data)];
memcpy(buf, &data, sizeof(data));
for(int i = 0; i < sizeof(data); i++) {
file.write(buf[i]);
}
file.close();
}
LittleFS.end();
return true;
}

View File

@ -18,7 +18,9 @@ struct EnergyAccountingData {
class EnergyAccounting {
public:
EnergyAccounting(RemoteDebug*);
void setup(AmsDataStorage *ds, EntsoeApi *eapi);
void setup(AmsDataStorage *ds, EntsoeApi *eapi, EnergyAccountingConfig *config);
void setTimezone(Timezone*);
EnergyAccountingConfig* getConfig();
bool update(AmsData* amsData);
bool save();
@ -32,7 +34,7 @@ public:
double getCostLastMonth();
float getMonthMax();
float getCurrentThreshold();
uint8_t getCurrentThreshold();
private:
RemoteDebug* debugger = NULL;
@ -40,12 +42,15 @@ private:
bool init = false, initPrice = false;
AmsDataStorage *ds = NULL;
EntsoeApi *eapi = NULL;
uint8_t thresholds[5] = {50, 100, 150, 200, 250};
EnergyAccountingConfig *config = NULL;
Timezone *tz = NULL;
uint8_t currentHour = 0, currentDay = 0, currentThresholdIdx = 0;
double use, costHour, costDay;
EnergyAccountingData data;
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0 };
bool load();
bool calcDayUse();
void calcDayCost();
};
#endif

View File

@ -5,6 +5,7 @@
#include <MQTT.h>
#include "AmsData.h"
#include "AmsConfiguration.h"
#include "EnergyAccounting.h"
#include "HwTools.h"
#include "entsoe/EntsoeApi.h"
@ -14,7 +15,7 @@ public:
this->mqtt = mqtt;
};
virtual bool publish(AmsData* data, AmsData* previousState);
virtual bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
virtual bool publishTemperatures(AmsConfiguration*, HwTools*);
virtual bool publishPrices(EntsoeApi* eapi);
virtual bool publishSystem(HwTools*);

View File

@ -1,7 +1,7 @@
#include "DomoticzMqttHandler.h"
#include "web/root/domoticz_json.h"
bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState) {
bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea) {
bool ret = false;
if (config.elidx > 0) {
if(data->getActiveImportCounter() > 1.0) {

View File

@ -9,7 +9,7 @@ public:
DomoticzMqttHandler(MQTTClient* mqtt, DomoticzConfig config) : AmsMqttHandler(mqtt) {
this->config = config;
};
bool publish(AmsData* data, AmsData* previousState);
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*);

View File

@ -8,7 +8,7 @@
#include "web/root/jsonsys_json.h"
#include "web/root/jsonprices_json.h"
bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState) {
bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea) {
if(topic.isEmpty() || !mqtt->connected())
return false;
@ -22,7 +22,9 @@ bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState) {
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature(),
data->getActiveImportPower()
data->getActiveImportPower(),
ea->getUseThisHour(),
ea->getCurrentThreshold()
);
return mqtt->publish(topic, json);
} else if(data->getListType() == 2) {
@ -47,7 +49,9 @@ bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState) {
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage()
data->getL3Voltage(),
ea->getUseThisHour(),
ea->getCurrentThreshold()
);
return mqtt->publish(topic, json);
} else if(data->getListType() == 3) {
@ -78,7 +82,9 @@ bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState) {
data->getActiveExportCounter(),
data->getReactiveImportCounter(),
data->getReactiveExportCounter(),
data->getMeterTimestamp()
data->getMeterTimestamp(),
ea->getUseThisHour(),
ea->getCurrentThreshold()
);
return mqtt->publish(topic, json);
} else {
@ -112,7 +118,9 @@ bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState) {
data->getActiveExportCounter(),
data->getReactiveImportCounter(),
data->getReactiveExportCounter(),
data->getMeterTimestamp()
data->getMeterTimestamp(),
ea->getUseThisHour(),
ea->getCurrentThreshold()
);
return mqtt->publish(topic, json);
}

View File

@ -10,7 +10,7 @@ public:
this->topic = String(topic);
this->hw = hw;
};
bool publish(AmsData* data, AmsData* previousState);
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*);

View File

@ -2,7 +2,7 @@
#include "hexutils.h"
#include "Uptime.h"
bool RawMqttHandler::publish(AmsData* data, AmsData* meterState) {
bool RawMqttHandler::publish(AmsData* data, AmsData* meterState, EnergyAccounting* ea) {
if(topic.isEmpty() || !mqtt->connected())
return false;
@ -71,6 +71,8 @@ bool RawMqttHandler::publish(AmsData* data, AmsData* meterState) {
mqtt->publish(topic + "/meter/import/active", String(data->getActiveImportPower()));
}
}
mqtt->publish(topic + "/realtime/import/hour", String(ea->getUseThisHour(), 3));
mqtt->publish(topic + "/realtime/import/threshold", String(ea->getCurrentThreshold(), 10), true, 0);
return true;
}

View File

@ -9,7 +9,7 @@ public:
this->topic = String(topic);
this->full = full;
};
bool publish(AmsData* data, AmsData* previousState);
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*);

View File

@ -47,6 +47,7 @@
#include "root/dayplot_json.h"
#include "root/monthplot_json.h"
#include "root/energyprice_json.h"
#include "root/energyaccounting_html.h"
#include "base64.h"
@ -79,6 +80,7 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, Meter
server.on("/web", HTTP_GET, std::bind(&AmsWebServer::configWebHtml, this));
server.on("/domoticz",HTTP_GET, std::bind(&AmsWebServer::configDomoticzHtml, this));
server.on("/entsoe",HTTP_GET, std::bind(&AmsWebServer::configEntsoeHtml, this));
server.on("/accounting",HTTP_GET, std::bind(&AmsWebServer::configEnergyAccountingHtml, this));
server.on("/boot.css", HTTP_GET, std::bind(&AmsWebServer::bootCss, this));
server.on("/github.svg", HTTP_GET, std::bind(&AmsWebServer::githubSvg, this));
server.on("/data.json", HTTP_GET, std::bind(&AmsWebServer::dataJson, this));
@ -647,6 +649,25 @@ void AmsWebServer::configEntsoeHtml() {
}
}
void AmsWebServer::configEnergyAccountingHtml() {
printD("Serving /accounting.html over http...");
if(!checkSecurity(1))
return;
EnergyAccountingConfig* config = ea->getConfig();
String html = String((const __FlashStringHelper*) ENERGYACCOUNTING_HTML);
for(int i = 0; i < 9; i++) {
html.replace("{t" + String(i) + "}", String(config->thresholds[i]));
}
server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN);
server.send_P(200, "text/html", HEAD_HTML);
server.sendContent(html);
server.sendContent_P(FOOT_HTML);
}
void AmsWebServer::configWebHtml() {
printD("Serving /web.html over http...");
@ -1286,6 +1307,21 @@ void AmsWebServer::handleSave() {
config->setEntsoeConfig(entsoe);
}
if(server.hasArg("cc") && server.arg("cc") == "true") {
printD("Received energy accounting config");
EnergyAccountingConfig eac;
eac.thresholds[0] = server.arg("t0").toInt();
eac.thresholds[1] = server.arg("t1").toInt();
eac.thresholds[2] = server.arg("t2").toInt();
eac.thresholds[3] = server.arg("t3").toInt();
eac.thresholds[4] = server.arg("t4").toInt();
eac.thresholds[5] = server.arg("t5").toInt();
eac.thresholds[6] = server.arg("t6").toInt();
eac.thresholds[7] = server.arg("t7").toInt();
eac.thresholds[8] = server.arg("t8").toInt();
config->setEnergyAccountingConfig(eac);
}
printI("Saving configuration now...");
//if (debugger->isActive(RemoteDebug::DEBUG)) config->print(debugger);

View File

@ -81,6 +81,7 @@ private:
void configNtpHtml();
void configGpioHtml();
void configDebugHtml();
void configEnergyAccountingHtml();
void bootCss();
void githubSvg();
void dataJson();

View File

@ -755,13 +755,16 @@ var fetch = function() {
if(json.ea) {
$('#each').html(json.ea.h.u.toFixed(2));
$('#eachc').html(json.ea.h.c.toFixed(2));
$('#eacd').html(json.ea.d.u.toFixed(2));
$('#eacdc').html(json.ea.d.c.toFixed(2));
$('#eacm').html(json.ea.m.u.toFixed(2));
$('#eacmc').html(json.ea.m.c.toFixed(2));
$('#eax').html(json.ea.x.toFixed(2));
$('#eat').html(json.ea.t.toFixed(2));
$('#eacd').html(json.ea.d.u.toFixed(1));
$('#eacdc').html(json.ea.d.c.toFixed(1));
$('#eacm').html(json.ea.m.u.toFixed(0));
$('#eacmc').html(json.ea.m.c.toFixed(0));
$('#eax').html(json.ea.x.toFixed(1));
$('#eat').html(json.ea.t.toFixed(0));
$('.cr').html(currency);
if(currency) {
$('.sp').show();
}
}
if(json.me) {

116
web/energyaccounting.html Normal file
View File

@ -0,0 +1,116 @@
<form method="post" action="/save">
<input type="hidden" name="cc" value="true"/>
<div class="my-3 p-3 bg-white rounded shadow">
<h6>Tariff thresholds</h6>
<div class="row">
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">1</span>
</div>
<input class="form-control text-right" name="t0" type="number" min="5" max="255" step="1" value="{t0}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">2</span>
</div>
<input class="form-control text-right" name="t1" type="number" min="5" max="255" step="1" value="{t1}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">3</span>
</div>
<input class="form-control text-right" name="t2" type="number" min="5" max="255" step="1" value="{t2}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">4</span>
</div>
<input class="form-control text-right" name="t3" type="number" min="5" max="255" step="1" value="{t3}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">5</span>
</div>
<input class="form-control text-right" name="t4" type="number" min="5" max="255" step="1" value="{t4}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">6</span>
</div>
<input class="form-control text-right" name="t5" type="number" min="5" max="255" step="1" value="{t5}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">7</span>
</div>
<input class="form-control text-right" name="t6" type="number" min="5" max="255" step="1" value="{t6}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">8</span>
</div>
<input class="form-control text-right" name="t7" type="number" min="5" max="255" step="1" value="{t7}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
<div class="m-2 input-group input-group-sm">
<div class="input-group-prepend">
<span class="input-group-text">9</span>
</div>
<input class="form-control text-right" name="t8" type="number" min="5" max="255" step="1" value="{t8}"/>
<div class="input-group-append">
<span class="input-group-text">kWh</span>
</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>

View File

@ -62,6 +62,7 @@
<a class="dropdown-item" href="/mqtt">MQTT</a>
<a class="dropdown-item" href="/web">Web</a>
<a class="dropdown-item" href="/ntp">NTP</a>
<a class="dropdown-item" href="/accounting">Thresholds</a>
<a class="dropdown-item" href="/entsoe">ENTSO-E API</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="https://github.com/gskjold/AmsToMqttBridge/wiki" target="_blank">Documentation</a>

View File

@ -62,6 +62,7 @@
<a class="dropdown-item" href="/mqtt">MQTT</a>
<a class="dropdown-item" href="/web">Web</a>
<a class="dropdown-item" href="/ntp">NTP</a>
<a class="dropdown-item" href="/accounting">Thresholds</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="https://github.com/gskjold/AmsToMqttBridge/wiki" target="_blank">Documentation</a>
</div>

View File

@ -113,14 +113,14 @@
<div class="col-xl-12 mb-3">
<div class="bg-white rounded shadow pt-3 pb-3" style="font-size: 14px;">
<strong class="mr-3 ml-3">Current use and cost</strong><br/>
<strong class="mr-3 ml-3">Real time calculation</strong><br/>
<div class="row">
<div class="col-lg-3 col-sm-6">
<div class="mr-3 ml-3 d-flex">
<div>Hour</div>
<div class="flex-fill text-right">
<span id="each"></span> kWh
<span class="sp text-nowrap">(<span id="eachc"></span> <span class="cr"></span>)</span>
<span class="sp text-nowrap" style="display: none;">(<span id="eachc"></span> <span class="cr"></span>)</span>
</div>
</div>
</div>
@ -129,7 +129,7 @@
<div>Day</div>
<div class="flex-fill text-right">
<span id="eacd"></span> kWh
<span class="sp text-nowrap">(<span id="eacdc"></span> <span class="cr"></span>)</span>
<span class="sp text-nowrap" style="display: none;">(<span id="eacdc"></span> <span class="cr"></span>)</span>
</div>
</div>
</div>
@ -138,14 +138,16 @@
<div>Month</div>
<div class="flex-fill text-right">
<span id="eacm"></span> kWh
<span class="sp text-nowrap">(<span id="eacmc"></span> <span class="cr"></span>)</span>
<span class="sp text-nowrap" style="display: none;">(<span id="eacmc"></span> <span class="cr"></span>)</span>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6">
<div class="row mr-3 ml-3">
<div class="col-3">Max</div>
<div class="col-9 text-right"><span id="eax"></span> / <span id="eat"></span> kWh</div>
<div class="mr-3 ml-3 d-flex">
<div>Max</div>
<div class="flex-fill text-right">
<span id="eax"></span> / <span id="eat"></span> kWh
</div>
</div>
</div>
</div>

View File

@ -8,5 +8,9 @@
"temp": %.2f,
"data" : {
"P" : %d
},
"realtime" : {
"h" : %.2f,
"t" : %d
}
}

View File

@ -20,5 +20,9 @@
"U1" : %.2f,
"U2" : %.2f,
"U3" : %.2f
},
"realtime" : {
"h" : %.2f,
"t" : %d
}
}

View File

@ -25,5 +25,9 @@
"tQI" : %.3f,
"tQO" : %.3f,
"rtc" : %lu
},
"realtime" : {
"h" : %.2f,
"t" : %d
}
}

View File

@ -29,5 +29,9 @@
"tQI" : %.2f,
"tQO" : %.2f,
"rtc" : %lu
},
"realtime" : {
"h" : %.2f,
"t" : %d
}
}