More changes for v2.2

This commit is contained in:
Gunnar Skjold 2023-01-11 20:41:27 +01:00
parent 4972b980ba
commit 870617f780
25 changed files with 508 additions and 179 deletions

View File

@ -11,6 +11,7 @@
#define CONFIG_SYSTEM_START 8
#define CONFIG_METER_START 32
#define CONFIG_UI_START 248
#define CONFIG_GPIO_START 266
#define CONFIG_ENTSOE_START 290
#define CONFIG_WIFI_START 360
@ -216,6 +217,20 @@ struct EnergyAccountingConfig {
uint8_t hours;
}; // 11
struct UiConfig {
uint8_t showImport;
uint8_t showExport;
uint8_t showVoltage;
uint8_t showAmperage;
uint8_t showReactive;
uint8_t showRealtime;
uint8_t showPeaks;
uint8_t showPricePlot;
uint8_t showDayPlot;
uint8_t showMonthPlot;
uint8_t showTemperaturePlot;
}; // 11
struct TempSensorConfig {
uint8_t address[8];
char name[16];
@ -292,6 +307,10 @@ public:
bool isEnergyAccountingChanged();
void ackEnergyAccountingChange();
bool getUiConfig(UiConfig&);
bool setUiConfig(UiConfig&);
void clearUiConfig(UiConfig&);
void loadTempSensors();
void saveTempSensors();
uint8_t getTempSensorCount();

View File

@ -549,7 +549,6 @@ bool AmsConfiguration::setEnergyAccountingConfig(EnergyAccountingConfig& config)
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
void AmsConfiguration::clearEnergyAccountingConfig(EnergyAccountingConfig& config) {
@ -574,6 +573,42 @@ void AmsConfiguration::ackEnergyAccountingChange() {
energyAccountingChanged = false;
}
bool AmsConfiguration::getUiConfig(UiConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_UI_START, config);
if(config.showImport > 2) clearUiConfig(config); // Must be wrong
EEPROM.end();
return true;
} else {
clearUiConfig(config);
return false;
}
}
bool AmsConfiguration::setUiConfig(UiConfig& config) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(CONFIG_UI_START, config);
bool ret = EEPROM.commit();
EEPROM.end();
return ret;
}
void AmsConfiguration::clearUiConfig(UiConfig& config) {
// 1 = Always, 2 = If value present, 0 = Hidden
config.showImport = 1;
config.showExport = 2;
config.showVoltage = 2;
config.showAmperage = 2;
config.showReactive = 0;
config.showRealtime = 1;
config.showPeaks = 2;
config.showPricePlot = 2;
config.showDayPlot = 1;
config.showMonthPlot = 1;
config.showTemperaturePlot = 2;
}
void AmsConfiguration::clear() {
EEPROM.begin(EEPROM_SIZE);
@ -621,6 +656,10 @@ void AmsConfiguration::clear() {
clearDebug(debug);
EEPROM.put(CONFIG_DEBUG_START, debug);
UiConfig ui;
clearUiConfig(ui);
EEPROM.put(CONFIG_UI_START, ui);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CLEARED_INDICATOR);
EEPROM.commit();
EEPROM.end();
@ -949,6 +988,10 @@ bool AmsConfiguration::relocateConfig100() {
EEPROM.put(CONFIG_METER_START, meter);
UiConfig ui;
clearUiConfig(ui);
EEPROM.put(CONFIG_UI_START, ui);
EEPROM.put(EEPROM_CONFIG_ADDRESS, 101);
bool ret = EEPROM.commit();
EEPROM.end();

View File

@ -37,8 +37,7 @@ private:
HTTPClient http;
uint8_t currentDay = 0, currentHour = 0;
uint32_t tomorrowFetchMillis = 36000000; // Number of ms before midnight. Default fetch 10hrs before midnight (14:00 CE(S)T)
uint64_t midnightMillis = 0;
uint8_t tomorrowFetchMinute = 15; // How many minutes over 13:00 should it fetch prices
uint64_t lastTodayFetch = 0;
uint64_t lastTomorrowFetch = 0;
uint64_t lastCurrencyFetch = 0;
@ -60,7 +59,7 @@ private:
PricesContainer* fetchPrices(time_t);
bool retrieve(const char* url, Stream* doc);
float getCurrencyMultiplier(const char* from, const char* to);
float getCurrencyMultiplier(const char* from, const char* to, time_t t);
void printD(String fmt, ...);
void printE(String fmt, ...);

View File

@ -21,7 +21,7 @@ EntsoeApi::EntsoeApi(RemoteDebug* Debug) {
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
tz = new Timezone(CEST, CET);
tomorrowFetchMillis = 36000000 + (random(1800) * 1000); // Random between 13:30 and 14:00
tomorrowFetchMinute = 15 + random(45); // Random between 13:15 and 14:00
}
void EntsoeApi::setup(EntsoeConfig& config) {
@ -96,7 +96,7 @@ float EntsoeApi::getValueForHour(time_t cur, int8_t hour) {
} else {
return ENTSOE_NO_VALUE;
}
float mult = getCurrencyMultiplier(tomorrow->currency, config->currency);
float mult = getCurrencyMultiplier(tomorrow->currency, config->currency, cur);
if(mult == 0) return ENTSOE_NO_VALUE;
multiplier *= mult;
} else if(pos >= 0) {
@ -112,7 +112,7 @@ float EntsoeApi::getValueForHour(time_t cur, int8_t hour) {
} else {
return ENTSOE_NO_VALUE;
}
float mult = getCurrencyMultiplier(today->currency, config->currency);
float mult = getCurrencyMultiplier(today->currency, config->currency, cur);
if(mult == 0) return ENTSOE_NO_VALUE;
multiplier *= mult;
}
@ -138,21 +138,15 @@ bool EntsoeApi::loop() {
if(strlen(config->currency) == 0)
return false;
bool ret = false;
tmElements_t tm;
breakTime(tz->toLocal(t), tm);
if(currentHour != tm.Hour) {
currentHour = tm.Hour;
ret = today != NULL; // Only trigger MQTT publish if we have todays prices.
}
if(midnightMillis == 0) {
uint32_t curDayMillis = (((((tm.Hour * 60) + tm.Minute) * 60) + tm.Second) * 1000);
midnightMillis = now + (SECS_PER_DAY * 1000) - curDayMillis;
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Setting midnight millis %llu\n", midnightMillis);
if(currentDay == 0) {
currentDay = tm.Day;
return false;
} else if(now > midnightMillis && currentDay != tm.Day) {
currentHour = tm.Hour;
}
if(currentDay != tm.Day) {
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Rotating price objects at %lu\n", t);
if(today != NULL) delete today;
if(tomorrow != NULL) {
@ -160,38 +154,38 @@ bool EntsoeApi::loop() {
tomorrow = NULL;
}
currentDay = tm.Day;
midnightMillis = 0; // Force new midnight millis calculation
return true;
} else {
if(today == NULL && (lastTodayFetch == 0 || now - lastTodayFetch > 60000)) {
try {
lastTodayFetch = now;
today = fetchPrices(t);
} catch(const std::exception& e) {
if(lastError == 0) lastError = 900;
today = NULL;
}
return today != NULL;
}
// Prices for next day are published at 13:00 CE(S)T, but to avoid heavy server traffic at that time, we will
// fetch 1 hr after that (with some random delay) and retry every 15 minutes
if(tomorrow == NULL
&& midnightMillis - now < tomorrowFetchMillis
&& (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 900000)
) {
try {
breakTime(t+SECS_PER_DAY, tm); // Break UTC tomorrow to find UTC midnight
lastTomorrowFetch = now;
tomorrow = fetchPrices(t+SECS_PER_DAY);
} catch(const std::exception& e) {
if(lastError == 0) lastError = 900;
tomorrow = NULL;
}
return tomorrow != NULL;
}
currentHour = tm.Hour;
return today != NULL; // Only trigger MQTT publish if we have todays prices.
} else if(currentHour != tm.Hour) {
currentHour = tm.Hour;
return today != NULL; // Only trigger MQTT publish if we have todays prices.
}
return ret;
if(today == NULL && (lastTodayFetch == 0 || now - lastTodayFetch > 60000)) {
try {
lastTodayFetch = now;
today = fetchPrices(t);
} catch(const std::exception& e) {
if(lastError == 0) lastError = 900;
today = NULL;
}
return today != NULL; // Only trigger MQTT publish if we have todays prices.
}
// Prices for next day are published at 13:00 CE(S)T, but to avoid heavy server traffic at that time, we will
// fetch with one hour (with some random delay) and retry every 15 minutes
if(tomorrow == NULL && (tm.Hour > 13 || (tm.Hour == 13 && tm.Minute >= tomorrowFetchMinute)) && (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 900000)) {
try {
lastTomorrowFetch = now;
tomorrow = fetchPrices(t+SECS_PER_DAY);
} catch(const std::exception& e) {
if(lastError == 0) lastError = 900;
tomorrow = NULL;
}
return tomorrow != NULL;
}
return false;
}
bool EntsoeApi::retrieve(const char* url, Stream* doc) {
@ -235,7 +229,7 @@ bool EntsoeApi::retrieve(const char* url, Stream* doc) {
return false;
}
float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to) {
float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to, time_t t) {
if(strcmp(from, to) == 0)
return 1.00;
@ -272,7 +266,9 @@ float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to) {
return 0;
}
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) Resulting currency multiplier: %.4f\n", currencyMultiplier);
lastCurrencyFetch = midnightMillis;
tmElements_t tm;
breakTime(t, tm);
lastCurrencyFetch = now + (SECS_PER_DAY * 1000) - (((((tm.Hour * 60) + tm.Minute) * 60) + tm.Second) * 1000);
}
return currencyMultiplier;
}

View File

@ -136,7 +136,7 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(strlen(eapi->getToken()) == 0)
if(eapi->getValueForHour(0) == ENTSOE_NO_VALUE)
return false;
time_t now = time(nullptr);

View File

@ -175,7 +175,7 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(strlen(eapi->getToken()) == 0)
if(eapi->getValueForHour(0) == ENTSOE_NO_VALUE)
return false;
time_t now = time(nullptr);

View File

@ -123,7 +123,7 @@ bool RawMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
bool RawMqttHandler::publishPrices(EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(strcmp(eapi->getToken(), "") == 0)
if(eapi->getValueForHour(0) == ENTSOE_NO_VALUE)
return false;
time_t now = time(nullptr);

View File

@ -33,7 +33,7 @@
<Router>
<Header data={data}/>
<Route path="/">
<Dashboard data={data}/>
<Dashboard data={data} sysinfo={sysinfo}/>
</Route>
<Route path="/configuration">
<ConfigurationPanel sysinfo={sysinfo}/>

View File

@ -18,7 +18,7 @@
if(u1 > 0) {
xTicks.push({ label: 'L1' });
points.push({
label: i1 ? i1 + 'A' : '-',
label: i1 ? (i1 > 10 ? i1.toFixed(0) : i1.toFixed(1)) + 'A' : '-',
value: i1 ? i1 : 0,
color: ampcol(i1 ? (i1)/(max)*100 : 0)
});
@ -26,7 +26,7 @@
if(u2 > 0) {
xTicks.push({ label: 'L2' });
points.push({
label: i2 ? i2 + 'A' : '-',
label: i2 ? (i2 > 10 ? i2.toFixed(0) : i2.toFixed(1)) + 'A' : '-',
value: i2 ? i2 : 0,
color: ampcol(i2 ? (i2)/(max)*100 : 0)
});
@ -34,7 +34,7 @@
if(u3 > 0) {
xTicks.push({ label: 'L3' });
points.push({
label: i3 ? i3 + 'A' : '-',
label: i3 ? (i3 > 10 ? i3.toFixed(0) : i3.toFixed(1)) + 'A' : '-',
value: i3 ? i3 : 0,
color: ampcol(i3 ? (i3)/(max)*100 : 0)
});

View File

@ -7,11 +7,13 @@
let xScale;
let yScale;
let heightAvailable;
let labelOffset;
$: {
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;
let yPerUnit = (heightAvailable-config.padding.top-config.padding.bottom)/(config.y.max-config.y.min);
@ -58,42 +60,45 @@
<g class='bars'>
{#each config.points as point, i}
{#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}"
/>
<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}"
/>
<text
y="{yScale(point.value) > yScale(0)-15 ? yScale(point.value) - 12 : 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)-15 ? point.color : 'white'}"
transform="rotate({barWidth < 25 ? 90 : 0}, {xScale(i) + (barWidth/2)}, {yScale(point.value) > yScale(0)-12 ? yScale(point.value) - 12 : yScale(point.value)+9})"
>{point.label}</text>
{#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}
{/if}
{#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}"
/>
<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>
<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}
{/if}
{/each}
</g>

View File

@ -47,6 +47,9 @@
d: {
s: false, t: false, l: 5
},
u: {
i: 0, e: 0, v: 0, a: 0, r: 0, c: 0, t: 0, p: 0, d: 0, m: 0, s: 0
},
i: {
h: null, a: null,
l: { p: null, i: false },
@ -94,6 +97,7 @@
sysinfoStore.update(s => {
s.booting = res.reboot;
s.ui = configuration.u;
return s;
});
@ -548,6 +552,100 @@
</label>
</div>
{/if}
<div class="cnt">
<strong class="text-sm">User interface</strong>
<input type="hidden" name="u" value="true"/>
<div class="flex flex-wrap">
<div class="w-1/2">
Import gauge<br/>
<select name="ui" bind:value={configuration.u.i} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Export gauge<br/>
<select name="ue" bind:value={configuration.u.e} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Voltage<br/>
<select name="uv" bind:value={configuration.u.v} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Amperage<br/>
<select name="ua" bind:value={configuration.u.a} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Reactive<br/>
<select name="ur" bind:value={configuration.u.r} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Realtime<br/>
<select name="uc" bind:value={configuration.u.c} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Peaks<br/>
<select name="ut" bind:value={configuration.u.t} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Price<br/>
<select name="up" bind:value={configuration.u.p} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Day plot<br/>
<select name="ud" bind:value={configuration.u.d} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Month plot<br/>
<select name="um" bind:value={configuration.u.m} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
<div class="w-1/2">
Temperature plot<br/>
<select name="us" bind:value={configuration.u.s} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
</div>
</div>
{#if sysinfo.board > 20 || sysinfo.chip == 'esp8266'}
<div class="cnt">
<strong class="text-sm">Hardware</strong>

View File

@ -1,6 +1,6 @@
<script>
import { pricesStore, dayPlotStore, monthPlotStore, temperaturesStore } from './DataStores.js';
import { metertype } from './Helpers.js';
import { metertype, uiVisibility } from './Helpers.js';
import PowerGauge from './PowerGauge.svelte';
import VoltPlot from './VoltPlot.svelte';
import AmpPlot from './AmpPlot.svelte';
@ -13,6 +13,7 @@
import TariffPeakChart from './TariffPeakChart.svelte';
export let data = {}
export let sysinfo = {}
let prices = {}
let dayPlot = {}
let monthPlot = {}
@ -32,6 +33,7 @@
</script>
<div class="grid xl:grid-cols-6 lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2">
{#if uiVisibility(sysinfo.ui.i, data.i)}
<div class="cnt">
<div class="grid grid-cols-2">
<div class="col-span-2">
@ -41,7 +43,8 @@
<div class="text-right">{data.ic ? data.ic.toFixed(1) : '-'} kWh</div>
</div>
</div>
{#if data.om || data.e > 0}
{/if}
{#if uiVisibility(sysinfo.ui.e, data.om || data.e > 0)}
<div class="cnt">
<div class="grid grid-cols-2">
<div class="col-span-2">
@ -52,39 +55,47 @@
</div>
</div>
{/if}
{#if data.u1 > 100 || data.u2 > 100 || data.u3 > 100}
{#if uiVisibility(sysinfo.ui.v, data.u1 > 100 || data.u2 > 100 || data.u3 > 100)}
<div class="cnt">
<VoltPlot u1={data.u1} u2={data.u2} u3={data.u3} ds={data.ds}/>
</div>
{/if}
{#if data.i1 > 0.01 || data.i2 > 0.01 || data.i3 > 0.01}
{#if uiVisibility(sysinfo.ui.a, data.i1 > 0.01 || data.i2 > 0.01 || data.i3 > 0.01)}
<div class="cnt">
<AmpPlot u1={data.u1} u2={data.u2} u3={data.u3} i1={data.i1} i2={data.i2} i3={data.i3} max={data.mf ? data.mf : 32}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.r, data.ri > 0 || data.re > 0 || data.ric > 0 || data.rec > 0)}
<div class="cnt">
<ReactiveData importInstant={data.ri} exportInstant={data.re} importTotal={data.ric} exportTotal={data.rec}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.c, data.ea)}
<div class="cnt">
<AccountingData data={data.ea} currency={prices.currency}/>
</div>
{/if}
{#if data && data.pr && (data.pr.startsWith("10YNO") || data.pr == '10Y1001A1001A48H')}
<div class="cnt h-64">
<TariffPeakChart />
</div>
{/if}
{#if (typeof data.p == "number") && !Number.isNaN(data.p)}
{#if uiVisibility(sysinfo.ui.p, (typeof data.p == "number") && !Number.isNaN(data.p))}
<div class="cnt xl:col-span-6 lg:col-span-4 md:col-span-3 sm:col-span-2 h-64">
<PricePlot json={prices}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.d, dayPlot)}
<div class="cnt xl:col-span-6 lg:col-span-4 md:col-span-3 sm:col-span-2 h-64">
<DayPlot json={dayPlot} />
</div>
{/if}
{#if uiVisibility(sysinfo.ui.m, monthPlot)}
<div class="cnt xl:col-span-6 lg:col-span-4 md:col-span-3 sm:col-span-2 h-64">
<MonthPlot json={monthPlot} />
</div>
{#if data.t && data.t != -127 && temperatures.c > 1}
{/if}
{#if uiVisibility(sysinfo.ui.s, data.t && data.t != -127 && temperatures.c > 1)}
<div class="cnt xl:col-span-6 lg:col-span-4 md:col-span-3 sm:col-span-2 h-64">
<TemperaturePlot json={temperatures} />
</div>

View File

@ -1,4 +1,5 @@
import { readable, writable } from 'svelte/store';
import { isBusPowered } from './Helpers';
async function fetchWithTimeout(resource, options = {}) {
const { timeout = 8000 } = options;
@ -16,11 +17,14 @@ async function fetchWithTimeout(resource, options = {}) {
let sysinfo = {
version: '',
chip: '',
mac: null,
apmac: null,
vndcfg: null,
usrcfg: null,
fwconsent: null,
booting: false,
upgrading: false,
ui: {},
security: 0
};
export const sysinfoStore = writable(sysinfo);
@ -43,18 +47,31 @@ export const dataStore = readable(data, (set) => {
set(data);
if(lastTemp != data.t) {
lastTemp = data.t;
getTemperatures();
setTimeout(getTemperatures, 2000);
}
if(lastPrice != data.p) {
lastPrice = data.p;
getPrices();
setTimeout(getPrices, 4000);
}
if(sysinfo.upgrading) {
window.location.reload();
} else if(sysinfo.booting) {
} else if(!sysinfo || !sysinfo.chip || sysinfo.booting || (tries > 1 && !isBusPowered(sysinfo.board))) {
getSysinfo();
if(dayPlotTimeout) clearTimeout(dayPlotTimeout);
dayPlotTimeout = setTimeout(getDayPlot, 2000);
if(monthPlotTimeout) clearTimeout(monthPlotTimeout);
monthPlotTimeout = setTimeout(getMonthPlot, 3000);
}
timeout = setTimeout(getData, 5000);
let to = 5000;
if(isBusPowered(sysinfo.board) && data.v > 2.5) {
let diff = (3.3 - Math.min(3.3, data.v));
if(diff > 0) {
to = Math.max(diff, 0.1) * 10 * 5000;
}
}
if(to > 5000) console.log("Scheduling next data fetch in " + to + "ms");
if(timeout) clearTimeout(timeout);
timeout = setTimeout(getData, to);
tries = 0;
})
.catch((err) => {
@ -68,7 +85,7 @@ export const dataStore = readable(data, (set) => {
});
timeout = setTimeout(getData, 15000);
} else {
timeout = setTimeout(getData, 5000);
timeout = setTimeout(getData, isBusPowered(sysinfo.board) ? 10000 : 5000);
}
});
}
@ -80,37 +97,49 @@ export const dataStore = readable(data, (set) => {
let prices = {};
export const pricesStore = writable(prices);
export async function getPrices(){
export async function getPrices() {
const response = await fetchWithTimeout("/energyprice.json");
prices = (await response.json())
pricesStore.set(prices);
}
let dayPlot = {};
export const dayPlotStore = readable(dayPlot, (set) => {
async function getDayPlot(){
const response = await fetchWithTimeout("/dayplot.json");
dayPlot = (await response.json())
set(dayPlot);
let date = new Date();
setTimeout(getDayPlot, (61-date.getMinutes())*60000)
let dayPlotTimeout;
export async function getDayPlot() {
if(dayPlotTimeout) {
clearTimeout(dayPlotTimeout);
dayPlotTimeout = 0;
}
const response = await fetchWithTimeout("/dayplot.json");
dayPlot = (await response.json())
dayPlotStore.set(dayPlot);
let date = new Date();
dayPlotTimeout = setTimeout(getDayPlot, ((60-date.getMinutes())*60000)+20)
}
export const dayPlotStore = writable(dayPlot, (set) => {
getDayPlot();
return function stop() {}
});
let monthPlot = {};
export const monthPlotStore = readable(monthPlot, (set) => {
async function getmonthPlot(){
const response = await fetchWithTimeout("/monthplot.json");
monthPlot = (await response.json())
set(monthPlot);
let date = new Date();
setTimeout(getmonthPlot, (24-date.getHours())*3600000)
let monthPlotTimeout;
export async function getMonthPlot() {
if(monthPlotTimeout) {
clearTimeout(monthPlotTimeout);
monthPlotTimeout = 0;
}
getmonthPlot();
const response = await fetchWithTimeout("/monthplot.json");
monthPlot = (await response.json())
monthPlotStore.set(monthPlot);
let date = new Date();
monthPlotTimeout = setTimeout(getMonthPlot, ((24-date.getHours())*3600000)+40)
}
export const monthPlotStore = writable(monthPlot, (set) => {
getMonthPlot();
return function stop() {}
});
@ -127,12 +156,17 @@ export const temperaturesStore = writable(temperatures, (set) => {
});
let tariff = {};
let tariffTimeout;
export async function getTariff() {
if(tariffTimeout) {
clearTimeout(tariffTimeout);
tariffTimeout = 0;
}
const response = await fetchWithTimeout("/tariff.json");
tariff = (await response.json())
tariffStore.set(tariff);
let date = new Date();
setTimeout(getTariff, (61-date.getMinutes())*60000)
tariffTimeout = setTimeout(getTariff, ((60-date.getMinutes())*60000)+30)
}
export const tariffStore = writable(tariff, (set) => {

View File

@ -2,7 +2,7 @@
import { Link } from "svelte-navigator";
import { sysinfoStore, getGitHubReleases, gitHubReleaseStore } from './DataStores.js';
import { upgrade, getNextVersion } from './UpgradeHelper';
import { boardtype, hanError, mqttError, priceError } from './Helpers.js';
import { boardtype, hanError, mqttError, priceError, isBusPowered } from './Helpers.js';
import GitHubLogo from './../assets/github.svg';
import Uptime from "./Uptime.svelte";
import Badge from './Badge.svelte';
@ -19,7 +19,7 @@
function askUpgrade() {
if(confirm('Do you want to upgrade this device to ' + nextVersion.tag_name + '?')) {
if((sysinfo.board != 2 && sysinfo.board != 4 && sysinfo.board != 7) || confirm('WARNING: ' + boardtype(sysinfo.chip, sysinfo.board) + ' must be connected to an external power supply during firmware upgrade. Failure to do so may cause power-down during upload resulting in non-functioning unit.')) {
if(!isBusPowered(sysinfo.board) || confirm('WARNING: ' + boardtype(sysinfo.chip, sysinfo.board) + ' must be connected to an external power supply during firmware upgrade. Failure to do so may cause power-down during upload resulting in non-functioning unit.')) {
sysinfoStore.update(s => {
s.upgrading = true;
return s;

View File

@ -143,4 +143,18 @@ export function priceError(err) {
if(err < 0) return "Unspecified error "+err;
return "";
}
}
export function isBusPowered(boardType) {
switch(boardType) {
case 2:
case 4:
case 7:
return true;
}
return false;
}
export function uiVisibility(choice, state) {
return choice == 1 || (choice == 2 && state);
}

View File

@ -76,8 +76,6 @@
});
}
console.log(yTicks);
config = {
title: "Future energy price (" + json.currency + ")",
padding: { top: 20, right: 15, bottom: 20, left: 35 },

View File

@ -46,6 +46,7 @@
const data = new URLSearchParams();
for (let field of formData) {
const [key, value] = field;
data.append(key, value)
if(key == 'sh') hostname = value;
}

View File

@ -1,5 +1,5 @@
<script>
import { metertype, boardtype } from './Helpers.js';
import { metertype, boardtype, isBusPowered } from './Helpers.js';
import { getSysinfo, gitHubReleaseStore, sysinfoStore } from './DataStores.js';
import { upgrade, getNextVersion } from './UpgradeHelper';
import DownloadIcon from './DownloadIcon.svelte';
@ -69,6 +69,11 @@
<div class="my-2">
MAC: {sysinfo.mac}
</div>
{#if sysinfo.apmac && sysinfo.apmac != sysinfo.mac}
<div class="my-2">
AP MAC: {sysinfo.apmac}
</div>
{/if}
<div class="my-2">
<Link to="/consent">
<span class="text-xs py-1 px-2 rounded bg-blue-500 text-white mr-3 ">Update consents</span>
@ -128,7 +133,7 @@
</div>
{/if}
{/if}
{#if (sysinfo.security == 0 || data.a) && (sysinfo.board == 2 || sysinfo.board == 4 || sysinfo.board == 7) }
{#if (sysinfo.security == 0 || data.a) && isBusPowered(sysinfo.board) }
<div class="bd-red">
{boardtype(sysinfo.chip, sysinfo.board)} must be connected to an external power supply during firmware upgrade. Failure to do so may cause power-down during upload resulting in non-functioning unit.
</div>

View File

@ -4,24 +4,26 @@
let hours = 0;
let minutes = 0;
$: {
days = Math.round(epoch/86400);
hours = Math.round(epoch/3600);
minutes = Math.round(epoch/60);
days = Math.floor(epoch/86400);
hours = Math.floor(epoch/3600);
minutes = Math.floor(epoch/60);
}
</script>
Up
{#if days > 1}
{days} days
{:else if days > 0}
{days} day
{:else if hours > 1}
{hours} hours
{:else if hours > 0}
{hours} hour
{:else if minutes > 1}
{minutes} minutes
{:else if minutes > 0}
{minutes} minute
{:else}
{epoch} seconds
{#if epoch}
Up
{#if days > 1}
{days} days
{:else if days > 0}
{days} day
{:else if hours > 1}
{hours} hours
{:else if hours > 0}
{hours} hour
{:else if minutes > 1}
{minutes} minutes
{:else if minutes > 0}
{minutes} minute
{:else}
{epoch} seconds
{/if}
{/if}

View File

@ -17,7 +17,7 @@
if(u1 > 0) {
xTicks.push({ label: ds === 1 ? 'L1-L2' : 'L1' });
points.push({
label: u1 ? u1 + 'V' : '-',
label: u1 ? u1.toFixed(0) + 'V' : '-',
value: u1 ? u1 : 0,
color: voltcol(u1 ? (u1-min)/(max-min)*100 : 0)
});
@ -25,7 +25,7 @@
if(u2 > 0) {
xTicks.push({ label: ds === 1 ? 'L1-L3' : 'L2' });
points.push({
label: u2 ? u2 + 'V' : '-',
label: u2 ? u2.toFixed(0) + 'V' : '-',
value: u2 ? u2 : 0,
color: voltcol(u2 ? (u2-min)/(max-min)*100 : 0)
});
@ -33,7 +33,7 @@
if(u3 > 0) {
xTicks.push({ label: ds === 1 ? 'L2-L3' : 'L3' });
points.push({
label: u3 ? u3 + 'V' : '-',
label: u3 ? u3.toFixed(0) + 'V' : '-',
value: u3 ? u3 : 0,
color: voltcol(u3 ? (u3-min)/(max-min)*100 : 0)
});

View File

@ -17,14 +17,14 @@ export default defineConfig({
plugins: [svelte()],
server: {
proxy: {
"/data.json": "https://ams2mqtt.no23.cc",
"/energyprice.json": "https://ams2mqtt.no23.cc",
"/dayplot.json": "https://ams2mqtt.no23.cc",
"/monthplot.json": "https://ams2mqtt.no23.cc",
"/temperature.json": "https://ams2mqtt.no23.cc",
"/sysinfo.json": "https://ams2mqtt.no23.cc",
"/data.json": "http://192.168.233.229",
"/energyprice.json": "http://192.168.233.229",
"/dayplot.json": "http://192.168.233.229",
"/monthplot.json": "http://192.168.233.229",
"/temperature.json": "http://192.168.233.229",
"/sysinfo.json": "http://192.168.233.229",
"/configuration.json": "http://192.168.233.229",
"/tariff.json": "https://ams2mqtt.no23.cc",
"/tariff.json": "http://192.168.233.229",
"/save": "http://192.168.233.229",
"/reboot": "http://192.168.233.229",
"/configfile": "http://192.168.233.229",

View File

@ -0,0 +1,13 @@
"u": {
"i": %d,
"e": %d,
"v": %d,
"a": %d,
"r": %d,
"c": %d,
"t": %d,
"p": %d,
"d": %d,
"m": %d,
"s": %d
},

View File

@ -3,6 +3,7 @@
"chip": "%s",
"chipId": "%s",
"mac": "%s",
"apmac": "%s",
"board": %d,
"vndcfg": %s,
"usrcfg": %s,
@ -22,5 +23,18 @@
"model": "%s",
"id": "%s"
},
"ui": {
"i": %d,
"e": %d,
"v": %d,
"a": %d,
"r": %d,
"c": %d,
"t": %d,
"p": %d,
"d": %d,
"m": %d,
"s": %d
},
"security": %d
}

View File

@ -26,12 +26,14 @@
#include "html/conf_debug_json.h"
#include "html/conf_gpio_json.h"
#include "html/conf_domoticz_json.h"
#include "html/conf_ui_json.h"
#include "html/firmware_html.h"
#include "version.h"
#if defined(ESP32)
#include <esp_task_wdt.h>
#include <esp_wifi.h>
#endif
@ -216,6 +218,26 @@ void AmsWebServer::sysinfoJson() {
IPAddress dns1 = WiFi.dnsIP(0);
IPAddress dns2 = WiFi.dnsIP(1);
char macStr[18] = { 0 };
char apMacStr[18] = { 0 };
uint8_t mac[6];
uint8_t apmac[6];
#if defined(ESP8266)
wifi_get_macaddr(STATION_IF, mac);
wifi_get_macaddr(SOFTAP_IF, apmac);
#elif defined(ESP32)
esp_wifi_get_mac((wifi_interface_t)ESP_IF_WIFI_STA, mac);
esp_wifi_get_mac((wifi_interface_t)ESP_IF_WIFI_AP, apmac);
#endif
sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
sprintf(apMacStr, "%02X:%02X:%02X:%02X:%02X:%02X", apmac[0], apmac[1], apmac[2], apmac[3], apmac[4], apmac[5]);
UiConfig ui;
config->getUiConfig(ui);
snprintf_P(buf, BufferSize, SYSINFO_JSON,
VERSION,
#if defined(CONFIG_IDF_TARGET_ESP32S2)
@ -228,7 +250,8 @@ void AmsWebServer::sysinfoJson() {
"esp8266",
#endif
chipIdStr.c_str(),
WiFi.macAddress().c_str(),
macStr,
apMacStr,
sys.boardType,
sys.vendorConfigured ? "true" : "false",
sys.userConfigured ? "true" : "false",
@ -249,6 +272,17 @@ void AmsWebServer::sysinfoJson() {
meterState->getMeterType(),
meterState->getMeterModel().c_str(),
meterState->getMeterId().c_str(),
ui.showImport,
ui.showExport,
ui.showVoltage,
ui.showAmperage,
ui.showReactive,
ui.showRealtime,
ui.showPeaks,
ui.showPricePlot,
ui.showDayPlot,
ui.showMonthPlot,
ui.showTemperaturePlot,
webConfig.security
);
@ -730,6 +764,8 @@ void AmsWebServer::configurationJson() {
config->getDebugConfig(debugConfig);
DomoticzConfig domo;
config->getDomoticzConfig(domo);
UiConfig ui;
config->getUiConfig(ui);
bool qsc = false;
bool qsr = false;
@ -857,6 +893,20 @@ void AmsWebServer::configurationJson() {
gpioConfig->vccBootLimit / 10.0
);
server.sendContent(buf);
snprintf_P(buf, BufferSize, CONF_UI_JSON,
ui.showImport,
ui.showExport,
ui.showVoltage,
ui.showAmperage,
ui.showReactive,
ui.showRealtime,
ui.showPeaks,
ui.showPricePlot,
ui.showDayPlot,
ui.showMonthPlot,
ui.showTemperaturePlot
);
server.sendContent(buf);
snprintf_P(buf, BufferSize, CONF_DOMOTICZ_JSON,
domo.elidx,
domo.cl1idx,
@ -1251,6 +1301,23 @@ void AmsWebServer::handleSave() {
config->setDebugConfig(debug);
}
if(server.hasArg(F("u")) && server.arg(F("u")) == F("true")) {
UiConfig ui;
config->getUiConfig(ui);
ui.showImport = server.arg(F("ui")).toInt();
ui.showExport = server.arg(F("ue")).toInt();
ui.showVoltage = server.arg(F("uv")).toInt();
ui.showAmperage = server.arg(F("ua")).toInt();
ui.showReactive = server.arg(F("ur")).toInt();
ui.showRealtime = server.arg(F("uc")).toInt();
ui.showPeaks = server.arg(F("ut")).toInt();
ui.showPricePlot = server.arg(F("up")).toInt();
ui.showDayPlot = server.arg(F("ud")).toInt();
ui.showMonthPlot = server.arg(F("um")).toInt();
ui.showTemperaturePlot = server.arg(F("us")).toInt();
config->setUiConfig(ui);
}
if(server.hasArg(F("p")) && server.arg(F("p")) == F("true")) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Received price API config"));

View File

@ -1124,7 +1124,6 @@ void WiFi_connect() {
#endif
MDNS.end();
WiFi.persistent(false);
WiFi.disconnect(true);
WiFi.softAPdisconnect(true);
WiFi.enableAP(false);
@ -1175,8 +1174,11 @@ void WiFi_connect() {
WiFi.hostname(wifi.hostname);
}
#endif
#if defined(ESP32)
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
#endif
WiFi.setAutoReconnect(true);
WiFi.persistent(true);
if(WiFi.begin(wifi.ssid, wifi.psk)) {
if(wifi.sleep <= 2) {
switch(wifi.sleep) {
@ -1536,54 +1538,62 @@ void configFileParse() {
char* buf = (char*) commonBuffer;
memset(buf, 0, 1024);
while((size = file.readBytesUntil('\n', buf, 1024)) > 0) {
for(uint16_t i = 0; i < size; i++) {
if(buf[i] < 32 || buf[i] > 126) {
memset(buf+i, 0, size-i);
debugD("Found non-ascii, shortening line from %d to %d", size, i);
size = i;
break;
}
}
if(strncmp_P(buf, PSTR("boardType "), 10) == 0) {
if(!lSys) { config.getSystemConfig(sys); lSys = true; };
sys.boardType = String(buf+10).toInt();
} else if(strncmp_P(buf, PSTR("ssid "), 5) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.ssid, buf+5, size-5);
strcpy(wifi.ssid, buf+5);
} else if(strncmp_P(buf, PSTR("psk "), 4) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.psk, buf+4, size-4);
strcpy(wifi.psk, buf+4);
} else if(strncmp_P(buf, PSTR("ip "), 3) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.ip, buf+3, size-3);
strcpy(wifi.ip, buf+3);
} else if(strncmp_P(buf, PSTR("gateway "), 8) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.gateway, buf+8, size-8);
strcpy(wifi.gateway, buf+8);
} else if(strncmp_P(buf, PSTR("subnet "), 7) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.subnet, buf+7, size-7);
strcpy(wifi.subnet, buf+7);
} else if(strncmp_P(buf, PSTR("dns1 "), 5) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.dns1, buf+5, size-5);
strcpy(wifi.dns1, buf+5);
} else if(strncmp_P(buf, PSTR("dns2 "), 5) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.dns2, buf+5, size-5);
strcpy(wifi.dns2, buf+5);
} else if(strncmp_P(buf, PSTR("hostname "), 9) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
memcpy(wifi.hostname, buf+9, size-9);
strcpy(wifi.hostname, buf+9);
} else if(strncmp_P(buf, PSTR("mdns "), 5) == 0) {
if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; };
wifi.mdns = String(buf+5).toInt() == 1;;
} else if(strncmp_P(buf, PSTR("mqttHost "), 9) == 0) {
if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; };
memcpy(mqtt.host, buf+9, size-9);
strcpy(mqtt.host, buf+9);
} else if(strncmp_P(buf, PSTR("mqttPort "), 9) == 0) {
if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; };
mqtt.port = String(buf+9).toInt();
} else if(strncmp_P(buf, PSTR("mqttClientId "), 13) == 0) {
if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; };
memcpy(mqtt.clientId, buf+13, size-13);
strcpy(mqtt.clientId, buf+13);
} else if(strncmp_P(buf, PSTR("mqttPublishTopic "), 17) == 0) {
if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; };
memcpy(mqtt.publishTopic, buf+17, size-17);
strcpy(mqtt.publishTopic, buf+17);
} else if(strncmp_P(buf, PSTR("mqttUsername "), 13) == 0) {
if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; };
memcpy(mqtt.username, buf+13, size-13);
strcpy(mqtt.username, buf+13);
} else if(strncmp_P(buf, PSTR("mqttPassword "), 13) == 0) {
if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; };
memcpy(mqtt.password, buf+13, size-13);
strcpy(mqtt.password, buf+13);
} else if(strncmp_P(buf, PSTR("mqttPayloadFormat "), 18) == 0) {
if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; };
mqtt.payloadFormat = String(buf+18).toInt();
@ -1595,10 +1605,10 @@ void configFileParse() {
web.security = String(buf+12).toInt();
} else if(strncmp_P(buf, PSTR("webUsername "), 12) == 0) {
if(!lWeb) { config.getWebConfig(web); lWeb = true; };
memcpy(web.username, buf+12, size-12);
strcpy(web.username, buf+12);
} else if(strncmp_P(buf, PSTR("webPassword "), 12) == 0) {
if(!lWeb) { config.getWebConfig(web); lWeb = true; };
memcpy(web.username, buf+12, size-12);
strcpy(web.username, buf+12);
} else if(strncmp_P(buf, PSTR("meterBaud "), 10) == 0) {
if(!lMeter) { config.getMeterConfig(meter); lMeter = true; };
meter.baud = String(buf+10).toInt();
@ -1697,19 +1707,19 @@ void configFileParse() {
ntp.dhcp = String(buf+8).toInt() == 1;
} else if(strncmp_P(buf, PSTR("ntpServer "), 10) == 0) {
if(!lNtp) { config.getNtpConfig(ntp); lNtp = true; };
memcpy(ntp.server, buf+10, size-10);
strcpy(ntp.server, buf+10);
} else if(strncmp_P(buf, PSTR("ntpTimezone "), 12) == 0) {
if(!lNtp) { config.getNtpConfig(ntp); lNtp = true; };
memcpy(ntp.timezone, buf+12, size-12);
strcpy(ntp.timezone, buf+12);
} else if(strncmp_P(buf, PSTR("entsoeToken "), 12) == 0) {
if(!lEntsoe) { config.getEntsoeConfig(entsoe); lEntsoe = true; };
memcpy(entsoe.token, buf+12, size-12);
strcpy(entsoe.token, buf+12);
} else if(strncmp_P(buf, PSTR("entsoeArea "), 11) == 0) {
if(!lEntsoe) { config.getEntsoeConfig(entsoe); lEntsoe = true; };
memcpy(entsoe.area, buf+11, size-11);
strcpy(entsoe.area, buf+11);
} else if(strncmp_P(buf, PSTR("entsoeCurrency "), 15) == 0) {
if(!lEntsoe) { config.getEntsoeConfig(entsoe); lEntsoe = true; };
memcpy(entsoe.currency, buf+15, size-15);
strcpy(entsoe.currency, buf+15);
} else if(strncmp_P(buf, PSTR("entsoeMultiplier "), 17) == 0) {
if(!lEntsoe) { config.getEntsoeConfig(entsoe); lEntsoe = true; };
entsoe.multiplier = String(buf+17).toDouble() * 1000;