Compare commits

..

17 Commits

Author SHA1 Message Date
Gunnar Skjold
9f7e174c7b Updated code that fixes broken DNS on IPv6 networks 2023-05-21 13:50:05 +02:00
Gunnar Skjold
104617afd2 Use device timezone in gui 2023-05-21 12:34:00 +02:00
Gunnar Skjold
959664f61d Even more changes for negative prices 2023-05-21 12:10:00 +02:00
Gunnar Skjold
5834b07393 Adjustments to avoid dns resolving in a closed network 2023-05-20 22:03:26 +02:00
Gunnar Skjold
8dbcf2424a Hack to fix DNS on networks with IPv6 2023-05-20 21:59:18 +02:00
Gunnar Skjold
74345046b1 Adjustments for negative prices 2023-05-20 21:58:57 +02:00
Gunnar Skjold
516c80e38c Fixed negative and 0 price 2023-05-16 22:04:51 +02:00
Gunnar Skjold
17bbac8670 Updated exchange rate api call 2023-05-05 19:34:48 +02:00
Gunnar Skjold
ccee3f505d Changes after testing 2023-05-05 18:30:30 +02:00
Gunnar Skjold
48eb640838 Changes after testing 2023-05-05 18:12:26 +02:00
Gunnar Skjold
90ebe3803d More adaptations 2023-05-04 17:31:16 +02:00
Gunnar Skjold
07ff4e2b0c Added last month in realtime data. Also increased precision on realtime data 2023-05-04 14:33:14 +02:00
Gunnar Skjold
7e011a184b Added delay for stability purposes 2023-05-04 12:12:08 +02:00
Gunnar Skjold
324459df97 Reduce verbose debug 2023-05-04 12:02:16 +02:00
Gunnar Skjold
2db38835c5 Fixed errors in SVG 2023-05-04 11:58:39 +02:00
Gunnar Skjold
804e43824b Fixed data storage update 2023-05-04 11:44:22 +02:00
Gunnar Skjold
60c7cea724 Extended range for multipliers 2023-05-03 07:51:11 +02:00
21 changed files with 381 additions and 212 deletions

View File

@@ -65,8 +65,10 @@ bool AmsDataStorage::update(AmsData* data) {
day.activeImport = importCounter;
day.activeExport = exportCounter;
day.lastMeterReadTime = now;
return true;
} else if(day.activeImport == 0 || now - day.lastMeterReadTime > 86400) {
if(debugger->isActive(RemoteDebug::VERBOSE)) {
debugger->printf_P(PSTR("(AmsDataStorage) %lu == 0 || %lu - %lu > 86400"), day.activeImport, now, day.lastMeterReadTime);
}
day.activeImport = importCounter;
day.activeExport = exportCounter;
day.lastMeterReadTime = now;
@@ -112,6 +114,9 @@ bool AmsDataStorage::update(AmsData* data) {
month.activeExport = exportCounter;
month.lastMeterReadTime = now;
} else if(month.activeImport == 0 || now - month.lastMeterReadTime > 2682000) {
if(debugger->isActive(RemoteDebug::VERBOSE)) {
debugger->printf_P(PSTR("(AmsDataStorage) %lu == 0 || %lu - %lu > 2682000"), month.activeImport, now, month.lastMeterReadTime);
}
month.activeImport = importCounter;
month.activeExport = exportCounter;
month.lastMeterReadTime = now;
@@ -574,19 +579,20 @@ bool AmsDataStorage::isHappy() {
bool AmsDataStorage::isDayHappy() {
time_t now = time(nullptr);
if(now < FirmwareVersion::BuildEpoch) return false;
tmElements_t tm, last;
if(now < day.lastMeterReadTime) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(AmsDataStorage) Day data timestamp %lu < %lu\n"), (int32_t) now, (int32_t) day.lastMeterReadTime);
return false;
}
breakTime(tz->toLocal(now), tm);
breakTime(tz->toLocal(day.lastMeterReadTime), last);
if(now-day.lastMeterReadTime > 3600) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(AmsDataStorage) Day data timestamp age %lu - %lu > 3600\n"), (int32_t) now, (int32_t) day.lastMeterReadTime);
return false;
}
if(tm.Hour > last.Hour) {
tmElements_t tm, last;
breakTime(tz->toLocal(now), tm);
breakTime(tz->toLocal(day.lastMeterReadTime), last);
if(tm.Hour != last.Hour) {
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf_P(PSTR("(AmsDataStorage) Day data hour of last timestamp %d > %d\n"), tm.Hour, last.Hour);
return false;
}

View File

@@ -12,6 +12,21 @@ struct EnergyAccountingPeak {
};
struct EnergyAccountingData {
uint8_t version;
uint8_t month;
uint32_t costYesterday;
uint32_t costThisMonth;
uint32_t costLastMonth;
uint32_t incomeYesterday;
uint32_t incomeThisMonth;
uint32_t incomeLastMonth;
uint32_t lastMonthImport;
uint32_t lastMonthExport;
uint8_t lastMonthAccuracy;
EnergyAccountingPeak peaks[5];
};
struct EnergyAccountingData5 {
uint8_t version;
uint8_t month;
uint16_t costYesterday;
@@ -32,7 +47,7 @@ struct EnergyAccountingData4 {
EnergyAccountingPeak peaks[5];
};
struct EnergyAccountingData1 {
struct EnergyAccountingData2 {
uint8_t version;
uint8_t month;
uint16_t maxHour;
@@ -57,22 +72,24 @@ public:
float getUseThisHour();
float getUseToday();
float getUseThisMonth();
float getUseLastMonth();
float getProducedThisHour();
float getProducedToday();
float getProducedThisMonth();
float getProducedLastMonth();
float getCostThisHour();
float getCostToday();
float getCostYesterday();
float getCostThisMonth();
uint16_t getCostLastMonth();
float getCostLastMonth();
float getIncomeThisHour();
float getIncomeToday();
float getIncomeYesterday();
float getIncomeThisMonth();
uint16_t getIncomeLastMonth();
float getIncomeLastMonth();
float getMonthMax();
uint8_t getCurrentThreshold();
@@ -95,7 +112,7 @@ private:
uint8_t currentHour = 0, currentDay = 0, currentThresholdIdx = 0;
float use = 0, costHour = 0, costDay = 0;
float produce = 0, incomeHour = 0, incomeDay = 0;
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0 };
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
float fixedPrice = 0;
String currency = "";

View File

@@ -49,9 +49,10 @@ bool EnergyAccounting::update(AmsData* amsData) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(EnergyAccounting) Initializing data at %lu\n"), (int32_t) now);
if(!load()) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(EnergyAccounting) Unable to load existing data\n"));
data = { 5, local.Month,
data = { 6, local.Month,
0, 0, 0, // Cost
0, 0, 0, // Income
0, 0, 0, // Last month import, export and accuracy
0, 0, // Peak 1
0, 0, // Peak 2
0, 0, // Peak 3
@@ -62,8 +63,8 @@ bool EnergyAccounting::update(AmsData* amsData) {
for(uint8_t i = 0; i < 5; i++) {
debugger->printf_P(PSTR("(EnergyAccounting) Peak hour from day %d: %d\n"), data.peaks[i].day, data.peaks[i].value*10);
}
debugger->printf_P(PSTR("(EnergyAccounting) Loaded cost yesterday: %.2f, this month: %d, last month: %d\n"), data.costYesterday / 10.0, data.costThisMonth, data.costLastMonth);
debugger->printf_P(PSTR("(EnergyAccounting) Loaded income yesterday: %.2f, this month: %d, last month: %d\n"), data.incomeYesterday / 10.0, data.incomeThisMonth, data.incomeLastMonth);
debugger->printf_P(PSTR("(EnergyAccounting) Loaded cost yesterday: %.2f, this month: %d, last month: %d\n"), data.costYesterday / 100.0, data.costThisMonth / 100.0, data.costLastMonth / 100.0);
debugger->printf_P(PSTR("(EnergyAccounting) Loaded income yesterday: %.2f, this month: %d, last month: %d\n"), data.incomeYesterday / 100.0, data.incomeThisMonth / 100.0, data.incomeLastMonth / 100.0);
}
init = true;
}
@@ -94,14 +95,15 @@ bool EnergyAccounting::update(AmsData* amsData) {
costHour = 0;
incomeHour = 0;
uint8_t prevDay = currentDay;
if(local.Day != currentDay) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(EnergyAccounting) New day %d\n"), local.Day);
data.costYesterday = costDay * 10;
data.costThisMonth += costDay;
data.costYesterday = costDay * 100;
data.costThisMonth += costDay * 100;
costDay = 0;
data.incomeYesterday = incomeDay * 10;
data.incomeThisMonth += incomeDay;
data.incomeYesterday = incomeDay * 100;
data.incomeThisMonth += incomeDay * 100;
incomeDay = 0;
currentDay = local.Day;
@@ -117,6 +119,23 @@ bool EnergyAccounting::update(AmsData* amsData) {
for(uint8_t i = 0; i < 5; i++) {
data.peaks[i] = { 0, 0 };
}
uint64_t totalImport = 0, totalExport = 0;
for(uint8_t i = 1; i <= prevDay; i++) {
totalImport += ds->getDayImport(i);
totalExport += ds->getDayExport(i);
}
uint8_t accuracy = 0;
uint64_t importUpdate = totalImport, exportUpdate = totalExport;
while(importUpdate > UINT32_MAX || exportUpdate > UINT32_MAX) {
accuracy++;
importUpdate = totalImport / pow(10, accuracy);
exportUpdate = totalExport / pow(10, accuracy);
}
data.lastMonthImport = importUpdate;
data.lastMonthExport = exportUpdate;
data.lastMonthAccuracy = accuracy;
data.month = local.Month;
currentThresholdIdx = 0;
ret = true;
@@ -203,12 +222,16 @@ float EnergyAccounting::getUseThisMonth() {
time_t now = time(nullptr);
if(now < FirmwareVersion::BuildEpoch) return 0.0;
float ret = 0;
for(uint8_t i = 0; i < currentDay; i++) {
for(uint8_t i = 1; i < currentDay; i++) {
ret += ds->getDayImport(i) / 1000.0;
}
return ret + getUseToday();
}
float EnergyAccounting::getUseLastMonth() {
return (data.lastMonthImport * pow(10, data.lastMonthAccuracy)) / 1000;
}
float EnergyAccounting::getProducedThisHour() {
return produce;
}
@@ -230,12 +253,15 @@ float EnergyAccounting::getProducedThisMonth() {
time_t now = time(nullptr);
if(now < FirmwareVersion::BuildEpoch) return 0.0;
float ret = 0;
for(uint8_t i = 0; i < currentDay; i++) {
for(uint8_t i = 1; i < currentDay; i++) {
ret += ds->getDayExport(i) / 1000.0;
}
return ret + getProducedToday();
}
float EnergyAccounting::getProducedLastMonth() {
return (data.lastMonthExport * pow(10, data.lastMonthAccuracy)) / 1000;
}
float EnergyAccounting::getCostThisHour() {
return costHour;
@@ -246,15 +272,15 @@ float EnergyAccounting::getCostToday() {
}
float EnergyAccounting::getCostYesterday() {
return data.costYesterday / 10.0;
return data.costYesterday / 100.0;
}
float EnergyAccounting::getCostThisMonth() {
return data.costThisMonth + getCostToday();
return (data.costThisMonth / 100.0) + getCostToday();
}
uint16_t EnergyAccounting::getCostLastMonth() {
return data.costLastMonth;
float EnergyAccounting::getCostLastMonth() {
return data.costLastMonth / 100.0;
}
float EnergyAccounting::getIncomeThisHour() {
@@ -266,15 +292,15 @@ float EnergyAccounting::getIncomeToday() {
}
float EnergyAccounting::getIncomeYesterday() {
return data.incomeYesterday / 10.0;
return data.incomeYesterday / 100.0;
}
float EnergyAccounting::getIncomeThisMonth() {
return data.incomeThisMonth + getIncomeToday();
return (data.incomeThisMonth / 100.0) + getIncomeToday();
}
uint16_t EnergyAccounting::getIncomeLastMonth() {
return data.incomeLastMonth;
float EnergyAccounting::getIncomeLastMonth() {
return data.incomeLastMonth / 100.0;
}
uint8_t EnergyAccounting::getCurrentThreshold() {
@@ -364,17 +390,35 @@ bool EnergyAccounting::load() {
file.readBytes(buf, file.size());
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(EnergyAccounting) Data version %d\n"), buf[0]);
if(buf[0] == 5) {
if(buf[0] == 6) {
EnergyAccountingData* data = (EnergyAccountingData*) buf;
memcpy(&this->data, data, sizeof(this->data));
ret = true;
} else if(buf[0] == 5) {
EnergyAccountingData5* data = (EnergyAccountingData5*) buf;
this->data = { 6, data->month,
((uint32_t) data->costYesterday) * 10,
((uint32_t) data->costThisMonth) * 100,
((uint32_t) data->costLastMonth) * 100,
((uint32_t) data->incomeYesterday) * 10,
((uint32_t) data->incomeThisMonth) * 100,
((uint32_t) data->incomeLastMonth) * 100,
0,0,0, // Last month import, export and accuracy
data->peaks[0].day, data->peaks[0].value,
data->peaks[1].day, data->peaks[1].value,
data->peaks[2].day, data->peaks[2].value,
data->peaks[3].day, data->peaks[3].value,
data->peaks[4].day, data->peaks[4].value
};
ret = true;
} else if(buf[0] == 4) {
EnergyAccountingData4* data = (EnergyAccountingData4*) buf;
this->data = { 5, data->month,
data->costYesterday,
data->costThisMonth,
data->costLastMonth,
((uint32_t) data->costYesterday) * 10,
((uint32_t) data->costThisMonth) * 100,
((uint32_t) data->costLastMonth) * 100,
0,0,0, // Income from production
0,0,0, // Last month import, export and accuracy
data->peaks[0].day, data->peaks[0].value,
data->peaks[1].day, data->peaks[1].value,
data->peaks[2].day, data->peaks[2].value,
@@ -385,8 +429,11 @@ bool EnergyAccounting::load() {
} else if(buf[0] == 3) {
EnergyAccountingData* data = (EnergyAccountingData*) buf;
this->data = { 5, data->month,
(uint16_t) (data->costYesterday / 10), (uint16_t) (data->costThisMonth / 100), (uint16_t) (data->costLastMonth / 100),
data->costYesterday * 10,
data->costThisMonth,
data->costLastMonth,
0,0,0, // Income from production
0,0,0, // Last month import, export and accuracy
data->peaks[0].day, data->peaks[0].value,
data->peaks[1].day, data->peaks[1].value,
data->peaks[2].day, data->peaks[2].value,
@@ -398,6 +445,7 @@ bool EnergyAccounting::load() {
data = { 5, 0,
0, 0, 0, // Cost
0,0,0, // Income from production
0,0,0, // Last month import, export and accuracy
0, 0, // Peak 1
0, 0, // Peak 2
0, 0, // Peak 3
@@ -405,11 +453,11 @@ bool EnergyAccounting::load() {
0, 0 // Peak 5
};
if(buf[0] == 2) {
EnergyAccountingData1* data = (EnergyAccountingData1*) buf;
EnergyAccountingData2* data = (EnergyAccountingData2*) buf;
this->data.month = data->month;
this->data.costYesterday = (uint16_t) (data->costYesterday / 10);
this->data.costThisMonth = (uint16_t) (data->costThisMonth / 100);
this->data.costLastMonth = (uint16_t) (data->costLastMonth / 100);
this->data.costYesterday = data->costYesterday * 10;
this->data.costThisMonth = data->costThisMonth;
this->data.costLastMonth = data->costLastMonth;
uint8_t b = 0;
for(uint8_t i = sizeof(this->data); i < file.size(); i+=2) {
this->data.peaks[b].day = b;
@@ -418,15 +466,6 @@ bool EnergyAccounting::load() {
if(b >= config->hours || b >= 5) break;
}
ret = true;
} else if(buf[0] == 1) {
EnergyAccountingData1* data = (EnergyAccountingData1*) buf;
this->data.month = data->month;
this->data.costYesterday = (uint16_t) (data->costYesterday / 10);
this->data.costThisMonth = (uint16_t) (data->costThisMonth / 100);
this->data.costLastMonth = (uint16_t) (data->costLastMonth / 100);
this->data.peaks[0].day = 1;
this->data.peaks[0].value = data->maxHour;
ret = true;
} else {
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf_P(PSTR("(EnergyAccounting) Unknown version\n"));
ret = false;

View File

@@ -272,14 +272,14 @@ float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to, time_t
ESP.wdtFeed();
#endif
snprintf_P(buf, BufferSize, PSTR("https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1"), from);
snprintf_P(buf, BufferSize, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), from);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(EntsoeApi) Retrieving %s to NOK conversion\n"), from);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(EntsoeApi) url: %s\n"), buf);
if(retrieve(buf, &p)) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(EntsoeApi) got exchange rate %.4f\n"), p.getValue());
currencyMultiplier = p.getValue();
if(strncmp(to, "NOK", 3) != 0) {
snprintf_P(buf, BufferSize, PSTR("https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1"), to);
snprintf_P(buf, BufferSize, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), to);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf_P(PSTR("(EntsoeApi) Retrieving %s to NOK conversion\n"), to);
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf_P(PSTR("(EntsoeApi) url: %s\n"), buf);
if(retrieve(buf, &p)) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,8 @@
}
.cnt {
@apply bg-white m-2 p-2 rounded shadow-lg
@apply bg-white m-2 p-2 rounded shadow-lg;
min-height: 268px;
}
.gwf {

View File

@@ -1,7 +1,7 @@
<script>
import { fmtnum } from "./Helpers";
export let sysinfo;
export let data;
export let currency;
export let hasExport;
@@ -9,7 +9,7 @@
let hasCost = false;
let cols = 3
$: {
hasCost = data && data.h && (data.h.c || data.d.c || data.m.c || data.h.i || data.d.i || data.m.i);
hasCost = data && data.h && (data.h.c > 0.01 || data.d.c > 0.01 || data.m.c > 0.01 || data.h.i > 0.01 || data.d.i > 0.01 || data.m.i > 0.01);
cols = hasCost ? 3 : 2;
}
</script>
@@ -31,6 +31,9 @@
<div>Month</div>
<div class="text-right">{fmtnum(data.m.u)} kWh</div>
{#if hasCost}<div class="text-right">{fmtnum(data.m.c)} {currency}</div>{/if}
<div>Last mo.</div>
<div class="text-right">{fmtnum(sysinfo.last_month.u)} kWh</div>
{#if hasCost}<div class="text-right">{fmtnum(sysinfo.last_month.c)} {currency}</div>{/if}
</div>
<strong>Export</strong>
<div class="grid grid-cols-{cols}">
@@ -43,6 +46,9 @@
<div>Month</div>
<div class="text-right">{fmtnum(data.m.p)} kWh</div>
{#if hasCost}<div class="text-right">{fmtnum(data.m.i)} {currency}</div>{/if}
<div>Last mo.</div>
<div class="text-right">{fmtnum(sysinfo.last_month.p)} kWh</div>
{#if hasCost}<div class="text-right">{fmtnum(sysinfo.last_month.i)} {currency}</div>{/if}
</div>
{:else}
<strong>Consumption</strong>
@@ -53,6 +59,8 @@
<div class="text-right">{fmtnum(data.d.u,1)} kWh</div>
<div>Month</div>
<div class="text-right">{fmtnum(data.m.u)} kWh</div>
<div>Last month</div>
<div class="text-right">{fmtnum(sysinfo.last_month.u)} kWh</div>
</div>
{#if hasCost}
<strong>Cost</strong>
@@ -63,6 +71,8 @@
<div class="text-right">{fmtnum(data.d.c,1)} {currency}</div>
<div>Month</div>
<div class="text-right">{fmtnum(data.m.c)} {currency}</div>
<div>Last month</div>
<div class="text-right">{fmtnum(sysinfo.last_month.c)} {currency}</div>
</div>
{/if}
{/if}

View File

@@ -8,12 +8,14 @@
let yScale;
let heightAvailable;
let labelOffset;
let labelOffset2;
$: {
heightAvailable = height-(config.title ? 20 : 0);
let innerWidth = width - (config.padding.left + config.padding.right);
barWidth = innerWidth / config.points.length;
labelOffset = barWidth < 25 ? 28 : 17;
labelOffset2 = barWidth < 25 ? 10 : 100;
let yPerUnit = (heightAvailable-config.padding.top-config.padding.bottom)/(config.y.max-config.y.min);
@@ -32,88 +34,96 @@
};
};
</script>
<div class="chart" bind:clientWidth={width} bind:clientHeight={height}>
{#if config.title}
<strong class="text-sm">{config.title}</strong>
{#if config.x.ticks && config.points && heightAvailable}
{#if config.title}
<strong class="text-sm">{config.title}</strong>
{/if}
<svg height="{heightAvailable}">
<!-- y axis -->
<g class="axis y-axis">
{#each config.y.ticks as tick}
{#if !isNaN(yScale(tick.value))}
<g class="tick tick-{tick.value} tick-{tick.color}" transform="translate(0, {yScale(tick.value)})">
<line x2="100%"></line>
<text y="-4" x={tick.align == 'right' ? '85%' : ''}>{tick.label}</text>
</g>
{/if}
{/each}
</g>
<!-- x axis -->
<g class="axis x-axis">
{#each config.x.ticks as point, i}
{#if !isNaN(xScale(i))}
<g class="tick" transform="translate({xScale(i)},{heightAvailable})">
{#if barWidth > 20 || i%2 == 0}
<text x="{barWidth/2}" y="-4">{point.label}</text>
{/if}
</g>
{/if}
{/each}
</g>
<g class='bars'>
{#each config.points as point, i}
{#if !isNaN(xScale(i)) && !isNaN(yScale(point.value))}
<g>
{#if point.value !== undefined}
<rect
x="{xScale(i) + 2}"
y="{yScale(point.value)}"
width="{barWidth - 4}"
height="{yScale(config.y.min) - yScale(Math.min(config.y.min, 0) + point.value)}"
fill="{point.color}"
/>
{#if barWidth > 15}
<text
y="{yScale(point.value) > yScale(0)-labelOffset ? yScale(point.value) - labelOffset : yScale(point.value)+10}"
x="{xScale(i) + barWidth/2}"
width="{barWidth - 4}"
dominant-baseline="middle"
text-anchor="{barWidth < 25 ? 'left' : 'middle'}"
fill="{yScale(point.value) > yScale(0)-labelOffset ? point.color : 'white'}"
transform="rotate({barWidth < 25 ? 90 : 0}, {xScale(i) + (barWidth/2)}, {yScale(point.value) > yScale(0)-labelOffset ? yScale(point.value) - labelOffset : yScale(point.value)+9})"
>{point.label}</text>
{#if point.title}
<title>{point.title}</title>
{/if}
{/if}
{/if}
</g>
<g>
{#if point.value2 > 0.0001}
<rect
x="{xScale(i) + 2}"
y="{yScale(0)}"
width="{barWidth - 4}"
height="{yScale(config.y.min) - yScale(config.y.min + point.value2)}"
fill="{point.color}"
/>
{#if barWidth > 15}
<text
y="{yScale(-point.value2) < yScale(0) + labelOffset2 ? yScale(0) - labelOffset+2 : yScale(-point.value2)-labelOffset}"
x="{xScale(i) + barWidth/2}"
width="{barWidth - 4}"
dominant-baseline="middle"
text-anchor="{'middle'}"
fill="{yScale(-point.value2) < yScale(0) + labelOffset2 ? point.color : 'white'}"
transform="rotate({barWidth < 25 ? 90 : 0}, {xScale(i) + (barWidth/2)}, {yScale(-point.value2) < yScale(0) + labelOffset2 ? yScale(0) - labelOffset+2 : yScale(-point.value2)-labelOffset})"
>{point.label2}</text>
{#if point.title2}
<title>{point.title2}</title>
{/if}
{/if}
{/if}
</g>
{/if}
{/each}
</g>
</svg>
{/if}
<svg height="{heightAvailable}">
<!-- y axis -->
<g class="axis y-axis">
{#each config.y.ticks as tick}
<g class="tick tick-{tick.value} tick-{tick.color}" transform="translate(0, {yScale(tick.value)})">
<line x2="100%"></line>
<text y="-4" x={tick.align == 'right' ? '85%' : ''}>{tick.label}</text>
</g>
{/each}
</g>
<!-- x axis -->
<g class="axis x-axis">
{#each config.x.ticks as point, i}
<g class="tick" transform="translate({xScale(i)},{heightAvailable})">
{#if barWidth > 20 || i%2 == 0}
<text x="{barWidth/2}" y="-4">{point.label}</text>
{/if}
</g>
{/each}
</g>
<g class='bars'>
{#each config.points as point, i}
<g>
{#if point.value !== undefined}
<rect
x="{xScale(i) + 2}"
y="{yScale(point.value)}"
width="{barWidth - 4}"
height="{yScale(config.y.min) - yScale(Math.min(config.y.min, 0) + point.value)}"
fill="{point.color}"
/>
{#if barWidth > 15}
<text
y="{yScale(point.value) > yScale(0)-labelOffset ? yScale(point.value) - labelOffset : yScale(point.value)+10}"
x="{xScale(i) + barWidth/2}"
width="{barWidth - 4}"
dominant-baseline="middle"
text-anchor="{barWidth < 25 ? 'left' : 'middle'}"
fill="{yScale(point.value) > yScale(0)-labelOffset ? point.color : 'white'}"
transform="rotate({barWidth < 25 ? 90 : 0}, {xScale(i) + (barWidth/2)}, {yScale(point.value) > yScale(0)-labelOffset ? yScale(point.value) - labelOffset : yScale(point.value)+9})"
>{point.label}</text>
{#if point.title}
<title>{point.title}</title>
{/if}
{/if}
{/if}
</g>
<g>
{#if point.value2 > 0.0001}
<rect
x="{xScale(i) + 2}"
y="{yScale(0)}"
width="{barWidth - 4}"
height="{yScale(config.y.min) - yScale(config.y.min + point.value2)}"
fill="{point.color}"
/>
{#if barWidth > 15}
<text
y="{yScale(-point.value2) < yScale(0)+12 ? yScale(-point.value2) + 12 : yScale(-point.value2)-10}"
x="{xScale(i) + barWidth/2}"
width="{barWidth - 4}"
dominant-baseline="middle"
text-anchor="{barWidth < 25 ? 'left' : 'middle'}"
fill="{yScale(-point.value2) < yScale(0)+12 ? point.color : 'white'}"
transform="rotate({barWidth < 25 ? 90 : 0}, {xScale(i) + (barWidth/2)}, {yScale(point.value2 - config.y.min) > yScale(0)-12 ? yScale(point.value2 - config.y.min) - 12 : yScale(point.value2 - config.y.min)+9})"
>{point.label2}</text>
{#if point.title2}
<title>{point.title2}</title>
{/if}
{/if}
{/if}
</g>
{/each}
</g>
</svg>
</div>

View File

@@ -1,12 +1,20 @@
<script>
import { zeropad, monthnames } from './Helpers.js';
import { zeropad, monthnames, addHours } from './Helpers.js';
export let timestamp;
export let fullTimeColor;
export let offset;
let showFull;
let adjusteTimestamp;
$:{
showFull = Math.abs(new Date().getTime()-timestamp.getTime()) < 300000;
addHours(timestamp, offset);
}
</script>
{#if Math.abs(new Date().getTime()-timestamp.getTime()) < 300000 }
{`${zeropad(timestamp.getDate())}. ${monthnames[timestamp.getMonth()]} ${zeropad(timestamp.getHours())}:${zeropad(timestamp.getMinutes())}`}
{#if showFull }
{`${zeropad(timestamp.getDate())}. ${monthnames[timestamp.getMonth()]} ${zeropad(timestamp.getUTCHours())}:${zeropad(timestamp.getMinutes())}`}
{:else}
<span class="{fullTimeColor}">{`${zeropad(timestamp.getDate())}.${zeropad(timestamp.getMonth()+1)}.${timestamp.getFullYear()} ${zeropad(timestamp.getHours())}:${zeropad(timestamp.getMinutes())}`}</span>
<span class="{fullTimeColor}">{`${zeropad(timestamp.getDate())}.${zeropad(timestamp.getMonth()+1)}.${timestamp.getFullYear()} ${zeropad(timestamp.getUTCHours())}:${zeropad(timestamp.getMinutes())}`}</span>
{/if}

View File

@@ -353,19 +353,19 @@
<div class="flex my-1">
<div class="w-1/4">
Watt<br/>
<input name="mmw" bind:value={configuration.m.m.w} type="number" min="0.00" max="655.35" step="0.01" class="in-f tr w-full"/>
<input name="mmw" bind:value={configuration.m.m.w} type="number" min="0.00" max="1000" step="0.001" class="in-f tr w-full"/>
</div>
<div class="w-1/4">
Volt<br/>
<input name="mmv" bind:value={configuration.m.m.v} type="number" min="0.00" max="655.35" step="0.01" class="in-m tr w-full"/>
<input name="mmv" bind:value={configuration.m.m.v} type="number" min="0.00" max="1000" step="0.001" class="in-m tr w-full"/>
</div>
<div class="w-1/4">
Amp<br/>
<input name="mma" bind:value={configuration.m.m.a} type="number" min="0.00" max="655.35" step="0.01" class="in-m tr w-full"/>
<input name="mma" bind:value={configuration.m.m.a} type="number" min="0.00" max="1000" step="0.001" class="in-m tr w-full"/>
</div>
<div class="w-1/4">
kWh<br/>
<input name="mmc" bind:value={configuration.m.m.c} type="number" min="0.00" max="655.35" step="0.01" class="in-l tr w-full"/>
<input name="mmc" bind:value={configuration.m.m.c} type="number" min="0.00" max="1000" step="0.001" class="in-l tr w-full"/>
</div>
</div>
{/if}

View File

@@ -72,7 +72,7 @@
{/if}
{#if uiVisibility(sysinfo.ui.c, data.ea)}
<div class="cnt">
<AccountingData data={data.ea} currency={data.pc} hasExport={data.om > 0 || data.e > 0}/>
<AccountingData sysinfo={sysinfo} data={data.ea} currency={data.pc} hasExport={data.om > 0 || data.e > 0}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.t, data.pr && (data.pr.startsWith("10YNO") || data.pr == '10Y1001A1001A48H'))}
@@ -82,17 +82,17 @@
{/if}
{#if uiVisibility(sysinfo.ui.p, data.pe && !Number.isNaN(data.p))}
<div class="cnt gwf">
<PricePlot json={prices}/>
<PricePlot json={prices} sysinfo={sysinfo}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.d, dayPlot)}
<div class="cnt gwf">
<DayPlot json={dayPlot} />
<DayPlot json={dayPlot} sysinfo={sysinfo}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.m, monthPlot)}
<div class="cnt gwf">
<MonthPlot json={monthPlot} />
<MonthPlot json={monthPlot} sysinfo={sysinfo}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.s, data.t && data.t != -127 && temperatures.c > 1)}

View File

@@ -59,7 +59,7 @@ export const dataStore = readable(data, (set) => {
}
if(lastPrice != data.p && data.pe) {
lastPrice = data.p;
setTimeout(getPrices, 4000);
setTimeout(getPrices, 1000);
}
if(sysinfo.upgrading) {
window.location.reload();

View File

@@ -3,6 +3,7 @@
import BarChart from './BarChart.svelte';
export let json;
export let sysinfo;
let config = {};
let max = 0;
@@ -15,6 +16,7 @@
let points = [];
let cur = addHours(new Date(), -24);
let currentHour = new Date().getUTCHours();
addHours(cur, sysinfo.clock_offset);
for(i = currentHour; i<24; i++) {
let imp = json["i"+zeropad(i)];
let exp = json["e"+zeropad(i)];
@@ -22,7 +24,7 @@
if(exp === undefined) exp = 0;
xTicks.push({
label: zeropad(cur.getHours())
label: zeropad(cur.getUTCHours())
});
points.push({
label: imp.toFixed(1),
@@ -44,7 +46,7 @@
if(exp === undefined) exp = 0;
xTicks.push({
label: zeropad(cur.getHours())
label: zeropad(cur.getUTCHours())
});
points.push({
label: imp.toFixed(1),

View File

@@ -73,7 +73,7 @@
<a class="float-right" href='https://github.com/UtilitechAS/amsreader-firmware' target='_blank' rel="noreferrer" aria-label="GitHub"><img class="gh-logo" src={GitHubLogo} alt="GitHub repo"/></a>
</div>
<div class="flex-none my-auto px-2">
<Clock timestamp={ data.c ? new Date(data.c * 1000) : new Date(0) } fullTimeColor="text-red-500" />
<Clock timestamp={ data.c ? new Date(data.c * 1000) : new Date(0) } offset={sysinfo.clock_offset} fullTimeColor="text-red-500" />
</div>
{#if sysinfo.vndcfg && sysinfo.usrcfg}
<div class="flex-none px-1 mt-1" title="Configuration">

View File

@@ -1,8 +1,9 @@
<script>
import { zeropad } from './Helpers.js';
import { zeropad, addHours } from './Helpers.js';
import BarChart from './BarChart.svelte';
export let json;
export let sysinfo;
let config = {};
let max = 0;
@@ -15,6 +16,8 @@
let points = [];
let cur = new Date();
let lm = new Date();
addHours(cur, sysinfo.clock_offset);
addHours(lm, sysinfo.clock_offset);
lm.setDate(0);
for(i = cur.getDate(); i<=lm.getDate(); i++) {

View File

@@ -3,6 +3,7 @@
import BarChart from './BarChart.svelte';
export let json;
export let sysinfo;
let config = {};
let max = 0;
@@ -18,16 +19,17 @@
let xTicks = [];
let points = [];
let cur = new Date();
addHours(cur, sysinfo.clock_offset);
for(i = hour; i<24; i++) {
val = json[zeropad(h++)];
if(val == null) break;
xTicks.push({
label: zeropad(cur.getHours())
label: zeropad(cur.getUTCHours())
});
points.push({
label: val > 0 ? val.toFixed(d) : '',
title: val > 0 ? val.toFixed(2) + ' ' + json.currency : '',
value: val > 0 ? Math.abs(val*100) : 0,
label: val >= 0 ? val.toFixed(d) : '',
title: val >= 0 ? val.toFixed(2) + ' ' + json.currency : '',
value: val >= 0 ? Math.abs(val*100) : 0,
label2: val < 0 ? val.toFixed(d) : '',
title2: val < 0 ? val.toFixed(2) + ' ' + json.currency : '',
value2: val < 0 ? Math.abs(val*100) : 0,
@@ -41,11 +43,11 @@
val = json[zeropad(h++)];
if(val == null) break;
xTicks.push({
label: zeropad(cur.getHours())
label: zeropad(cur.getUTCHours())
});
points.push({
label: val > 0 ? val.toFixed(d) : '',
value: val > 0 ? Math.abs(val*100) : 0,
label: val >= 0 ? val.toFixed(d) : '',
value: val >= 0 ? Math.abs(val*100) : 0,
label2: val < 0 ? val.toFixed(d) : '',
value2: val < 0 ? Math.abs(val*100) : 0,
color: '#7c3aed'
@@ -55,12 +57,13 @@
addHours(cur, 1);
};
max = Math.ceil(max);
min = Math.floor(min);
let range = Math.max(max, Math.abs(min));
if(min < 0) {
let yTickDistDown = min/4;
for(i = 1; i < 5; i++) {
min = Math.min((range/4)*-1, min);
let yTicksNum = Math.ceil((Math.abs(min)/range) * 4);
let yTickDistDown = min/yTicksNum;
for(i = 1; i < yTicksNum+1; i++) {
let val = (yTickDistDown*i);
yTicks.push({
value: val,
@@ -69,8 +72,10 @@
}
}
let yTickDistUp = max/4;
for(i = 0; i < 5; i++) {
max = Math.max((range/4), max);
let xTicksNum = Math.ceil((max/range) * 4);
let yTickDistUp = max/xTicksNum;
for(i = 0; i < xTicksNum+1; i++) {
let val = (yTickDistUp*i);
yTicks.push({
value: val,

View File

@@ -17,18 +17,18 @@ export default defineConfig({
plugins: [svelte()],
server: {
proxy: {
"/data.json": "http://192.168.233.235",
"/energyprice.json": "http://192.168.233.235",
"/dayplot.json": "http://192.168.233.235",
"/monthplot.json": "http://192.168.233.235",
"/temperature.json": "http://192.168.233.235",
"/sysinfo.json": "http://192.168.233.235",
"/configuration.json": "http://192.168.233.235",
"/tariff.json": "http://192.168.233.235",
"/save": "http://192.168.233.235",
"/reboot": "http://192.168.233.235",
"/configfile": "http://192.168.233.235",
"/upgrade": "http://192.168.233.235"
"/data.json": "http://192.168.28.100",
"/energyprice.json": "http://192.168.28.100",
"/dayplot.json": "http://192.168.28.100",
"/monthplot.json": "http://192.168.28.100",
"/temperature.json": "http://192.168.28.100",
"/sysinfo.json": "http://192.168.28.100",
"/configuration.json": "http://192.168.28.100",
"/tariff.json": "http://192.168.28.100",
"/save": "http://192.168.28.100",
"/reboot": "http://192.168.28.100",
"/configfile": "http://192.168.28.100",
"/upgrade": "http://192.168.28.100"
}
}
})

View File

@@ -44,5 +44,12 @@
"e": %d,
"f": "%s",
"t": "%s"
}
},
"last_month": {
"u" : %.2f,
"c" : %.2f,
"p" : %.2f,
"i" : %.2f
},
"clock_offset": %d
}

View File

@@ -261,6 +261,8 @@ void AmsWebServer::sysinfoJson() {
if(!meterId.isEmpty())
meterId.replace(F("\\"), F("\\\\"));
time_t now = time(nullptr);
int size = snprintf_P(buf, BufferSize, SYSINFO_JSON,
FirmwareVersion::VersionString,
#if defined(CONFIG_IDF_TARGET_ESP32S2)
@@ -322,7 +324,12 @@ void AmsWebServer::sysinfoJson() {
upinfo.exitCode,
upinfo.errorCode,
upinfo.fromVersion,
upinfo.toVersion
upinfo.toVersion,
ea->getUseLastMonth(),
ea->getCostLastMonth(),
ea->getProducedLastMonth(),
ea->getIncomeLastMonth(),
(tz->toLocal(now)-now)/3600
);
stripNonAscii((uint8_t*) buf, size+1);
@@ -2186,15 +2193,15 @@ void AmsWebServer::configFileDownload() {
EnergyAccountingConfig eac;
config->getEnergyAccountingConfig(eac);
EnergyAccountingData ead = ea->getData();
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("energyaccounting %d %d %.2f %d %d %.2f %d %d %d %.2f %d %.2f %d %.2f %d %.2f %d %.2f"),
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("energyaccounting %d %d %.2f %.2f %.2f %.2f %.2f %.2f %d %.2f %d %.2f %d %.2f %d %.2f %d %.2f %.2f %.2f"),
ead.version,
ead.month,
ead.costYesterday / 10.0,
ead.costThisMonth,
ead.costLastMonth,
ead.incomeYesterday / 10.0,
ead.incomeThisMonth,
ead.incomeLastMonth,
ea->getCostYesterday(),
ea->getCostThisMonth(),
ea->getCostLastMonth(),
ea->getIncomeYesterday(),
ea->getIncomeThisMonth(),
ea->getIncomeLastMonth(),
ead.peaks[0].day,
ead.peaks[0].value / 100.0,
ead.peaks[1].day,
@@ -2204,7 +2211,9 @@ void AmsWebServer::configFileDownload() {
ead.peaks[3].day,
ead.peaks[3].value / 100.0,
ead.peaks[4].day,
ead.peaks[4].value / 100.0
ead.peaks[4].value / 100.0,
ea->getUseLastMonth(),
ea->getProducedLastMonth()
));
server.sendContent_P(PSTR("\n"));
}

View File

@@ -31,6 +31,7 @@ ADC_MODE(ADC_VCC);
#if defined(ESP32)
#include <esp_task_wdt.h>
#include <lwip/dns.h>
#endif
#define WDT_TIMEOUT 60
@@ -158,6 +159,30 @@ void printHanReadError(int pos);
void debugPrint(byte *buffer, int start, int length);
#if defined(ESP32)
uint8_t dnsState = 0;
ip_addr_t dns0;
void WiFiEvent(WiFiEvent_t event) {
switch(event) {
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
const ip_addr_t* dns = dns_getserver(0);
memcpy(&dns0, dns, sizeof(dns0));
IPAddress res;
int ret = WiFi.hostByName("hub.amsleser.no", res);
if(ret == 0) {
dnsState = 2;
debugI_P(PSTR("No DNS, probably a closed network"));
} else {
debugI_P(PSTR("DNS is present and working, monitoring"));
dnsState = 1;
}
break;
}
}
#endif
void setup() {
Serial.begin(115200);
@@ -408,7 +433,6 @@ bool wifiConnected = false;
unsigned long lastTemperatureRead = 0;
unsigned long lastSysupdate = 0;
unsigned long lastErrorBlink = 0;
unsigned long lastDataStoreUpdate = 0;
int lastError = 0;
bool meterAutodetect = false;
@@ -546,7 +570,7 @@ void loop() {
debugW_P(PSTR("Used %dms to read HAN port (false)"), millis()-start);
}
}
if(now > lastDataStoreUpdate && now - lastDataStoreUpdate > 3600000 && !ds.isHappy()) {
if(millis() - meterState.getLastUpdateMillis() > 1800000 && !ds.isHappy()) {
handleClear(now);
}
} catch(const std::exception& e) {
@@ -586,7 +610,6 @@ void handleClear(unsigned long now) {
AmsData nullData;
debugI_P(PSTR("Clearing data that have not been updated"));
ds.update(&nullData);
lastDataStoreUpdate = now;
}
}
@@ -634,6 +657,16 @@ void handleSystem(unsigned long now) {
if(end - start > 1000) {
debugW_P(PSTR("Used %dms to send system update to MQTT"), millis()-start);
}
#if defined(ESP32)
if(dnsState == 1) {
const ip_addr_t* dns = dns_getserver(0);
if(memcmp(&dns0, dns, sizeof(dns0)) != 0) {
dns_setserver(0, &dns0);
debugI_P(PSTR("Had to reset DNS server"));
}
}
#endif
}
}
@@ -970,6 +1003,7 @@ void swapWifiMode() {
WiFi.disconnect(true);
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_OFF);
delay(10);
yield();
if (mode != WIFI_AP || !config.hasConfig()) {
@@ -1183,11 +1217,11 @@ void handleDataSuccess(AmsData* data) {
meterState.apply(*data);
bool saveData = false;
if(!ds.isDayHappy() && now > FirmwareVersion::BuildEpoch) {
if(!ds.isHappy() && now > FirmwareVersion::BuildEpoch) { // Must use "isHappy()" in case day state gets reset and lastTimestamp is "now"
debugD_P(PSTR("Its time to update data storage"));
tmElements_t tm;
breakTime(now, tm);
if(tm.Minute == 0) {
if(tm.Minute == 0 && data->getListType() >= 3) {
debugV_P(PSTR(" using actual data"));
saveData = ds.update(data);
} else if(tm.Minute == 1 && meterState.getListType() >= 3) {
@@ -1197,7 +1231,6 @@ void handleDataSuccess(AmsData* data) {
if(saveData) {
debugI_P(PSTR("Saving data"));
ds.save();
lastDataStoreUpdate = millis();
}
}
@@ -1330,6 +1363,7 @@ void WiFi_connect() {
if(strlen(wifi.hostname) > 0) {
WiFi.setHostname(wifi.hostname);
}
WiFi.onEvent(WiFiEvent);
#endif
WiFi.mode(WIFI_STA);
@@ -2106,6 +2140,7 @@ void configFileParse() {
EnergyAccountingData ead = { 0, 0,
0, 0, 0, // Cost
0, 0, 0, // Income
0, 0, 0, // Last month import, export and accuracy
0, 0, // Peak 1
0, 0, // Peak 2
0, 0, // Peak 3
@@ -2113,6 +2148,7 @@ void configFileParse() {
0, 0 // Peak 5
};
uint8_t peak = 0;
uint64_t totalImport = 0, totalExport = 0;
char * pch = strtok (buf+17," ");
while (pch != NULL) {
if(ead.version < 5) {
@@ -2129,13 +2165,13 @@ void configFileParse() {
}
} else if(i == 3) {
float val = String(pch).toFloat();
ead.costYesterday = val * 10;
ead.costYesterday = val * 100;
} else if(i == 4) {
float val = String(pch).toFloat();
ead.costThisMonth = val;
ead.costThisMonth = val * 100;
} else if(i == 5) {
float val = String(pch).toFloat();
ead.costLastMonth = val;
ead.costLastMonth = val * 100;
} else if(i >= 6 && i < 18) {
uint8_t hour = i-6;
{
@@ -2156,23 +2192,23 @@ void configFileParse() {
ead.month = val;
} else if(i == 2) {
float val = String(pch).toFloat();
ead.costYesterday = val * 10;
ead.costYesterday = val * 100;
} else if(i == 3) {
float val = String(pch).toFloat();
ead.costThisMonth = val;
ead.costThisMonth = val * 100;
} else if(i == 4) {
float val = String(pch).toFloat();
ead.costLastMonth = val;
ead.costLastMonth = val * 100;
} else if(i == 5) {
float val = String(pch).toFloat();
ead.incomeYesterday= val * 10;
ead.incomeYesterday= val * 100;
} else if(i == 6) {
float val = String(pch).toFloat();
ead.incomeThisMonth = val;
ead.incomeThisMonth = val * 100;
} else if(i == 7) {
float val = String(pch).toFloat();
ead.incomeLastMonth = val;
} else if(i >= 8 && i < 20) {
ead.incomeLastMonth = val * 100;
} else if(i >= 8 && i < 18) {
uint8_t hour = i-8;
{
long val = String(pch).toInt();
@@ -2185,12 +2221,28 @@ void configFileParse() {
ead.peaks[peak].value = val * 100;
}
peak++;
} else if(i == 18) {
float val = String(pch).toFloat();
totalImport = val * 1000;
} else if(i == 19) {
float val = String(pch).toFloat();
totalExport = val * 1000;
}
}
pch = strtok (NULL, " ");
i++;
}
ead.version = 5;
uint8_t accuracy = 0;
uint64_t importUpdate = totalImport, exportUpdate = totalExport;
while(importUpdate > UINT32_MAX || exportUpdate > UINT32_MAX) {
accuracy++;
importUpdate = totalImport / pow(10, accuracy);
exportUpdate = totalExport / pow(10, accuracy);
}
ead.lastMonthImport = importUpdate;
ead.lastMonthExport = exportUpdate;
ead.version = 6;
ea.setData(ead);
sEa = true;
}