diff --git a/src/AmsConfiguration.cpp b/src/AmsConfiguration.cpp index 62819081..2b6e43f8 100644 --- a/src/AmsConfiguration.cpp +++ b/src/AmsConfiguration.cpp @@ -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; diff --git a/src/AmsConfiguration.h b/src/AmsConfiguration.h index 192eca3c..e2d1ed77 100644 --- a/src/AmsConfiguration.h +++ b/src/AmsConfiguration.h @@ -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 diff --git a/src/AmsDataStorage.cpp b/src/AmsDataStorage.cpp index 7fae4673..f6652496 100644 --- a/src/AmsDataStorage.cpp +++ b/src/AmsDataStorage.cpp @@ -1,6 +1,5 @@ #include "AmsDataStorage.h" #include -#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); diff --git a/src/AmsStorage.h b/src/AmsStorage.h index f0815d8a..0d6f0118 100644 --- a/src/AmsStorage.h +++ b/src/AmsStorage.h @@ -9,5 +9,6 @@ #define FILE_DAYPLOT "/dayplot.bin" #define FILE_MONTHPLOT "/monthplot.bin" +#define FILE_ENERGYACCOUNTING "/energyaccounting.bin" #endif diff --git a/src/AmsToMqttBridge.ino b/src/AmsToMqttBridge.ino index 7fa06e2b..7549e4d9 100644 --- a/src/AmsToMqttBridge.ino +++ b/src/AmsToMqttBridge.ino @@ -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); } diff --git a/src/EnergyAccounting.cpp b/src/EnergyAccounting.cpp index bf360262..29750d7f 100644 --- a/src/EnergyAccounting.cpp +++ b/src/EnergyAccounting.cpp @@ -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; } diff --git a/src/EnergyAccounting.h b/src/EnergyAccounting.h index e73be9de..8408a2f5 100644 --- a/src/EnergyAccounting.h +++ b/src/EnergyAccounting.h @@ -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 diff --git a/src/mqtt/AmsMqttHandler.h b/src/mqtt/AmsMqttHandler.h index f627ba36..10272f7a 100644 --- a/src/mqtt/AmsMqttHandler.h +++ b/src/mqtt/AmsMqttHandler.h @@ -5,6 +5,7 @@ #include #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*); diff --git a/src/mqtt/DomoticzMqttHandler.cpp b/src/mqtt/DomoticzMqttHandler.cpp index 262b2d92..96bffc18 100644 --- a/src/mqtt/DomoticzMqttHandler.cpp +++ b/src/mqtt/DomoticzMqttHandler.cpp @@ -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) { diff --git a/src/mqtt/DomoticzMqttHandler.h b/src/mqtt/DomoticzMqttHandler.h index 408f0d75..118a4286 100644 --- a/src/mqtt/DomoticzMqttHandler.h +++ b/src/mqtt/DomoticzMqttHandler.h @@ -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*); diff --git a/src/mqtt/JsonMqttHandler.cpp b/src/mqtt/JsonMqttHandler.cpp index ba76422f..e2bf8989 100644 --- a/src/mqtt/JsonMqttHandler.cpp +++ b/src/mqtt/JsonMqttHandler.cpp @@ -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); } diff --git a/src/mqtt/JsonMqttHandler.h b/src/mqtt/JsonMqttHandler.h index c8f45d03..65236d1c 100644 --- a/src/mqtt/JsonMqttHandler.h +++ b/src/mqtt/JsonMqttHandler.h @@ -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*); diff --git a/src/mqtt/RawMqttHandler.cpp b/src/mqtt/RawMqttHandler.cpp index 1f13d7f0..f5da3516 100644 --- a/src/mqtt/RawMqttHandler.cpp +++ b/src/mqtt/RawMqttHandler.cpp @@ -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; } diff --git a/src/mqtt/RawMqttHandler.h b/src/mqtt/RawMqttHandler.h index f312875c..4880066e 100644 --- a/src/mqtt/RawMqttHandler.h +++ b/src/mqtt/RawMqttHandler.h @@ -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*); diff --git a/src/web/AmsWebServer.cpp b/src/web/AmsWebServer.cpp index 166bd8a5..774eece0 100644 --- a/src/web/AmsWebServer.cpp +++ b/src/web/AmsWebServer.cpp @@ -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); diff --git a/src/web/AmsWebServer.h b/src/web/AmsWebServer.h index 218c2650..97905c2b 100644 --- a/src/web/AmsWebServer.h +++ b/src/web/AmsWebServer.h @@ -81,6 +81,7 @@ private: void configNtpHtml(); void configGpioHtml(); void configDebugHtml(); + void configEnergyAccountingHtml(); void bootCss(); void githubSvg(); void dataJson(); diff --git a/web/application.js b/web/application.js index e45c22f2..2b45092e 100644 --- a/web/application.js +++ b/web/application.js @@ -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) { diff --git a/web/energyaccounting.html b/web/energyaccounting.html new file mode 100644 index 00000000..9cba3203 --- /dev/null +++ b/web/energyaccounting.html @@ -0,0 +1,116 @@ +
+ +
+
Tariff thresholds
+
+
+
+
+ 1 +
+ +
+ kWh +
+
+
+
+
+
+ 2 +
+ +
+ kWh +
+
+
+
+
+
+ 3 +
+ +
+ kWh +
+
+
+
+
+
+ 4 +
+ +
+ kWh +
+
+
+
+
+
+ 5 +
+ +
+ kWh +
+
+
+
+
+
+ 6 +
+ +
+ kWh +
+
+
+
+
+
+ 7 +
+ +
+ kWh +
+
+
+
+
+
+ 8 +
+ +
+ kWh +
+
+
+
+
+
+ 9 +
+ +
+ kWh +
+
+
+
+
+
+
+
+ Back +
+
+ +
+
+
\ No newline at end of file diff --git a/web/head32.html b/web/head32.html index b21acd52..4d2b5f48 100644 --- a/web/head32.html +++ b/web/head32.html @@ -62,6 +62,7 @@ MQTT Web NTP + Thresholds ENTSO-E API Documentation diff --git a/web/head8266.html b/web/head8266.html index 36d85ad4..2ff5036e 100644 --- a/web/head8266.html +++ b/web/head8266.html @@ -62,6 +62,7 @@ MQTT Web NTP + Thresholds Documentation diff --git a/web/index.html b/web/index.html index ad64c17f..12d78349 100644 --- a/web/index.html +++ b/web/index.html @@ -113,14 +113,14 @@
- Current use and cost
+ Real time calculation
Hour
kWh - ( ) +
@@ -129,7 +129,7 @@
Day
kWh - ( ) +
@@ -138,14 +138,16 @@
Month
kWh - ( ) +
-
-
Max
-
/ kWh
+
+
Max
+
+ / kWh +
diff --git a/web/json1.json b/web/json1.json index c9c542ea..83c6ffca 100644 --- a/web/json1.json +++ b/web/json1.json @@ -8,5 +8,9 @@ "temp": %.2f, "data" : { "P" : %d + }, + "realtime" : { + "h" : %.2f, + "t" : %d } } diff --git a/web/json2.json b/web/json2.json index 9b6fbd24..c28a9793 100644 --- a/web/json2.json +++ b/web/json2.json @@ -20,5 +20,9 @@ "U1" : %.2f, "U2" : %.2f, "U3" : %.2f + }, + "realtime" : { + "h" : %.2f, + "t" : %d } } diff --git a/web/json3.json b/web/json3.json index 584503f0..081ba293 100644 --- a/web/json3.json +++ b/web/json3.json @@ -25,5 +25,9 @@ "tQI" : %.3f, "tQO" : %.3f, "rtc" : %lu + }, + "realtime" : { + "h" : %.2f, + "t" : %d } } diff --git a/web/json3pf.json b/web/json3pf.json index 679f53e9..6d1a5e72 100644 --- a/web/json3pf.json +++ b/web/json3pf.json @@ -29,5 +29,9 @@ "tQI" : %.2f, "tQO" : %.2f, "rtc" : %lu + }, + "realtime" : { + "h" : %.2f, + "t" : %d } }