mirror of
https://github.com/UtilitechAS/amsreader-firmware.git
synced 2026-03-03 18:27:06 +00:00
First implementation of energy accounting
This commit is contained in:
@@ -23,6 +23,7 @@ ADC_MODE(ADC_VCC);
|
||||
#include "AmsToMqttBridge.h"
|
||||
#include "AmsStorage.h"
|
||||
#include "AmsDataStorage.h"
|
||||
#include "EnergyAccounting.h"
|
||||
#include <MQTT.h>
|
||||
#include <DNSServer.h>
|
||||
#include <lwip/apps/sntp.h>
|
||||
@@ -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;
|
||||
|
||||
182
src/EnergyAccounting.cpp
Normal file
182
src/EnergyAccounting.cpp
Normal file
@@ -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
|
||||
}
|
||||
51
src/EnergyAccounting.h
Normal file
51
src/EnergyAccounting.h
Normal file
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
<div class="flex-fill">
|
||||
<div class="text-center">Free mem: <span class="jm">{mem}</span>kb</div>
|
||||
</div>
|
||||
<div class="flex-fill rc">
|
||||
<div class="flex-fill rc">
|
||||
<div class="text-center"><span class="jc"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,6 +111,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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/>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6">
|
||||
<div class="mr-3 ml-3 d-flex">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6">
|
||||
<div class="mr-3 ml-3 d-flex">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ppc" class="col-xl-12 mb-3" style="display: none;">
|
||||
<div class="bg-white rounded shadow" id="pp" style="width: 100%; height: 224px;"></div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user