diff --git a/src/AmsToMqttBridge.ino b/src/AmsToMqttBridge.ino index 3bc5f680..d758f62c 100644 --- a/src/AmsToMqttBridge.ino +++ b/src/AmsToMqttBridge.ino @@ -23,6 +23,7 @@ ADC_MODE(ADC_VCC); #include "AmsToMqttBridge.h" #include "AmsStorage.h" #include "AmsDataStorage.h" +#include "EnergyAccounting.h" #include #include #include @@ -80,6 +81,7 @@ AmsData meterState; bool ntpEnabled = false; AmsDataStorage ds(&Debug); +EnergyAccounting ea(&Debug); uint8_t wifiReconnectCount = 0; @@ -309,7 +311,8 @@ void setup() { swapWifiMode(); } - ws.setup(&config, &gpioConfig, &meterConfig, &meterState, &ds); + ea.setup(&ds, eapi); + ws.setup(&config, &gpioConfig, &meterConfig, &meterState, &ds, &ea); #if defined(ESP32) esp_task_wdt_init(WDT_TIMEOUT, true); @@ -872,6 +875,10 @@ bool readHanPort() { debugI("Saving day plot"); ds.save(); } + if(ea.update(&data)) { + debugI("Saving energy accounting"); + ea.save(); + } } delay(1); return true; diff --git a/src/EnergyAccounting.cpp b/src/EnergyAccounting.cpp new file mode 100644 index 00000000..bf360262 --- /dev/null +++ b/src/EnergyAccounting.cpp @@ -0,0 +1,182 @@ +#include "EnergyAccounting.h" + +EnergyAccounting::EnergyAccounting(RemoteDebug* debugger) { + this->debugger = debugger; +} + +void EnergyAccounting::setup(AmsDataStorage *ds, EntsoeApi *eapi) { + this->ds = ds; + this->eapi = eapi; +} + +bool EnergyAccounting::update(AmsData* amsData) { + time_t now = time(nullptr); + if(now < EPOCH_2021_01_01) return false; + + bool ret = false; + tmElements_t tm; + breakTime(now, tm); + + if(!init) { + 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; + } + } + } + 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; + } + + 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); + } + } + } + + 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; + } + } + + use = 0; + costHour = 0; + currentHour = tm.Hour; + + if(tm.Day != currentDay) { + if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New day %d\n", tm.Day); + data.costYesterday = costDay * 100; + data.costThisMonth += costDay * 100; + costDay = 0; + currentDay = tm.Day; + ret = true; + } + + if(tm.Month != data.month) { + if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New month %d\n", tm.Month); + data.costLastMonth = data.costThisMonth; + data.costThisMonth = 0; + data.maxHour = 0; + data.month = tm.Month; + currentThresholdIdx = 0; + ret = true; + } + } + + unsigned long ms = this->lastUpdateMillis > amsData->getLastUpdateMillis() ? 0 : amsData->getLastUpdateMillis() - this->lastUpdateMillis; + 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); + 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()); + 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); + + return ret; +} + +double EnergyAccounting::getUseThisHour() { + return use; +} + +double EnergyAccounting::getCostThisHour() { + return costHour; +} + +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; + } + return ret + getUseThisHour(); +} + +double EnergyAccounting::getCostToday() { + return costDay; +} + +double EnergyAccounting::getCostYesterday() { + return data.costYesterday / 100.0; +} + +double EnergyAccounting::getUseThisMonth() { + time_t now = time(nullptr); + if(now < EPOCH_2021_01_01) return 0; + tmElements_t tm; + breakTime(now, tm); + float ret = 0; + for(int i = 0; i < tm.Day; i++) { + ret += ds->getDay(i) / 1000.0; + } + return ret + getUseToday(); +} + +double EnergyAccounting::getCostThisMonth() { + return (data.costThisMonth / 100.0) + getCostToday(); +} + +double EnergyAccounting::getCostLastMonth() { + return data.costLastMonth / 100.0; +} + +float EnergyAccounting::getCurrentThreshold() { + return thresholds[currentThresholdIdx] / 10.0; +} + +float EnergyAccounting::getMonthMax() { + return data.maxHour / 100.0; +} + +bool EnergyAccounting::load() { + return false; // TODO +} + +bool EnergyAccounting::save() { + return false; // TODO +} diff --git a/src/EnergyAccounting.h b/src/EnergyAccounting.h new file mode 100644 index 00000000..e73be9de --- /dev/null +++ b/src/EnergyAccounting.h @@ -0,0 +1,51 @@ +#ifndef _ENERGYACCOUNTING_H +#define _ENERGYACCOUNTING_H + +#include "Arduino.h" +#include "AmsData.h" +#include "AmsDataStorage.h" +#include "entsoe/EntsoeApi.h" + +struct EnergyAccountingData { + uint8_t version; + uint8_t month; + uint16_t maxHour; + uint16_t costYesterday; + uint16_t costThisMonth; + uint16_t costLastMonth; +}; + +class EnergyAccounting { +public: + EnergyAccounting(RemoteDebug*); + void setup(AmsDataStorage *ds, EntsoeApi *eapi); + bool update(AmsData* amsData); + bool save(); + + double getUseThisHour(); + double getCostThisHour(); + double getUseToday(); + double getCostToday(); + double getCostYesterday(); + double getUseThisMonth(); + double getCostThisMonth(); + double getCostLastMonth(); + + float getMonthMax(); + float getCurrentThreshold(); + +private: + RemoteDebug* debugger = NULL; + unsigned long lastUpdateMillis = 0; + bool init = false, initPrice = false; + AmsDataStorage *ds = NULL; + EntsoeApi *eapi = NULL; + uint8_t thresholds[5] = {50, 100, 150, 200, 250}; + uint8_t currentHour = 0, currentDay = 0, currentThresholdIdx = 0; + double use, costHour, costDay; + EnergyAccountingData data; + + bool load(); +}; + +#endif diff --git a/src/entsoe/EntsoeApi.cpp b/src/entsoe/EntsoeApi.cpp index 1eeec954..594446e4 100644 --- a/src/entsoe/EntsoeApi.cpp +++ b/src/entsoe/EntsoeApi.cpp @@ -38,12 +38,12 @@ char* EntsoeApi::getCurrency() { return this->config->currency; } -float EntsoeApi::getValueForHour(uint8_t hour) { +float EntsoeApi::getValueForHour(int8_t hour) { time_t cur = time(nullptr); return getValueForHour(cur, hour); } -float EntsoeApi::getValueForHour(time_t cur, uint8_t hour) { +float EntsoeApi::getValueForHour(time_t cur, int8_t hour) { tmElements_t tm; if(tz != NULL) cur = tz->toLocal(cur); diff --git a/src/entsoe/EntsoeApi.h b/src/entsoe/EntsoeApi.h index 9ec5d788..e36a43c3 100644 --- a/src/entsoe/EntsoeApi.h +++ b/src/entsoe/EntsoeApi.h @@ -26,8 +26,8 @@ public: char* getToken(); char* getCurrency(); - float getValueForHour(uint8_t); - float getValueForHour(time_t, uint8_t); + float getValueForHour(int8_t); + float getValueForHour(time_t, int8_t); private: RemoteDebug* debugger; diff --git a/src/mqtt/JsonMqttHandler.cpp b/src/mqtt/JsonMqttHandler.cpp index 4f713217..ba76422f 100644 --- a/src/mqtt/JsonMqttHandler.cpp +++ b/src/mqtt/JsonMqttHandler.cpp @@ -122,7 +122,7 @@ bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState) { bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) { int count = hw->getTempSensorCount(); - if(count == 0) + if(count < 2) return false; int size = 32 + (count * 26); diff --git a/src/web/AmsWebServer.cpp b/src/web/AmsWebServer.cpp index b854bbfd..638abb34 100644 --- a/src/web/AmsWebServer.cpp +++ b/src/web/AmsWebServer.cpp @@ -1,6 +1,5 @@ #include "AmsWebServer.h" #include "version.h" -#include "AmsStorage.h" #include "hexutils.h" #include "AmsData.h" @@ -56,12 +55,13 @@ AmsWebServer::AmsWebServer(RemoteDebug* Debug, HwTools* hw) { this->hw = hw; } -void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, MeterConfig* meterConfig, AmsData* meterState, AmsDataStorage* ds) { +void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, MeterConfig* meterConfig, AmsData* meterState, AmsDataStorage* ds, EnergyAccounting* ea) { this->config = config; this->gpioConfig = gpioConfig; this->meterConfig = meterConfig; this->meterState = meterState; this->ds = ds; + this->ea = ea; char jsuri[32]; snprintf(jsuri, 32, "/application-%s.js", VERSION); @@ -757,7 +757,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, @@ -793,7 +793,15 @@ void AmsWebServer::dataJson() { price == ENTSOE_NO_VALUE ? "null" : String(price, 2).c_str(), time(nullptr), meterState->getMeterType(), - meterConfig->distributionSystem + meterConfig->distributionSystem, + ea->getMonthMax(), + ea->getCurrentThreshold(), + ea->getUseThisHour(), + ea->getCostThisHour(), + ea->getUseToday(), + ea->getCostToday(), + ea->getUseThisMonth(), + ea->getCostThisMonth() ); server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); @@ -1040,7 +1048,7 @@ void AmsWebServer::handleSetup() { break; case 200: // ESP32 gpioConfig->hanPin = 16; - gpioConfig->apPin = 0; + gpioConfig->apPin = 4; gpioConfig->ledPin = 2; gpioConfig->ledInverted = false; break; diff --git a/src/web/AmsWebServer.h b/src/web/AmsWebServer.h index ba4d952f..218c2650 100644 --- a/src/web/AmsWebServer.h +++ b/src/web/AmsWebServer.h @@ -8,7 +8,9 @@ #include "AmsConfiguration.h" #include "HwTools.h" #include "AmsData.h" +#include "AmsStorage.h" #include "AmsDataStorage.h" +#include "EnergyAccounting.h" #include "Uptime.h" #include "RemoteDebug.h" #include "entsoe/EntsoeApi.h" @@ -30,7 +32,7 @@ class AmsWebServer { public: AmsWebServer(RemoteDebug* Debug, HwTools* hw); - void setup(AmsConfiguration*, GpioConfig*, MeterConfig*, AmsData*, AmsDataStorage*); + void setup(AmsConfiguration*, GpioConfig*, MeterConfig*, AmsData*, AmsDataStorage*, EnergyAccounting*); void loop(); void setMqtt(MQTTClient* mqtt); void setTimezone(Timezone* tz); @@ -50,6 +52,7 @@ private: WebConfig webConfig; AmsData* meterState; AmsDataStorage* ds; + EnergyAccounting* ea = NULL; MQTTClient* mqtt = NULL; bool uploading = false; File file; diff --git a/web/application.js b/web/application.js index 6fab93d1..e45c22f2 100644 --- a/web/application.js +++ b/web/application.js @@ -1,6 +1,7 @@ var nextVersion; var im, em; var ds = 0; +var currency = ""; // Price plot var pp; @@ -432,6 +433,7 @@ var drawPrices = function() { timeout: 30000, dataType: 'json', }).done(function(json) { + currency = json.currency; data = [['Hour',json.currency + '/kWh', { role: 'style' }, { role: 'annotation' }]]; var r = 1; var hour = moment.utc().hours(); @@ -750,6 +752,18 @@ 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)); + $('.cr').html(currency); + } + if(json.me) { $('.me').addClass('d-none'); $('.me'+json.me).removeClass('d-none'); diff --git a/web/data.json b/web/data.json index d48cc336..5dd1d3a1 100644 --- a/web/data.json +++ b/web/data.json @@ -33,5 +33,21 @@ "p" : %s, "c" : %lu, "mt" : %d, - "ds" : %d + "ds" : %d, + "ea" : { + "x" : %.1f, + "t" : %.1f, + "h" : { + "u" : %.2f, + "c" : %.2f + }, + "d" : { + "u" : %.2f, + "c" : %.2f + }, + "m" : { + "u" : %.2f, + "c" : %.2f + } + } } \ No newline at end of file diff --git a/web/index.html b/web/index.html index 5f7c0404..ad64c17f 100644 --- a/web/index.html +++ b/web/index.html @@ -15,7 +15,7 @@
Free mem: {mem}kb
-
+
@@ -111,6 +111,47 @@ +
+
+ Current use and cost
+
+
+
+
Hour
+
+ kWh + ( ) +
+
+
+
+
+
Day
+
+ kWh + ( ) +
+
+
+
+
+
Month
+
+ kWh + ( ) +
+
+
+
+
+
Max
+
/ kWh
+
+
+
+
+
+