diff --git a/lib/AmsConfiguration/include/AmsConfiguration.h b/lib/AmsConfiguration/include/AmsConfiguration.h index c66885ac..1980378f 100644 --- a/lib/AmsConfiguration/include/AmsConfiguration.h +++ b/lib/AmsConfiguration/include/AmsConfiguration.h @@ -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(); diff --git a/lib/AmsConfiguration/src/AmsConfiguration.cpp b/lib/AmsConfiguration/src/AmsConfiguration.cpp index f88f7ea1..9001342f 100644 --- a/lib/AmsConfiguration/src/AmsConfiguration.cpp +++ b/lib/AmsConfiguration/src/AmsConfiguration.cpp @@ -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(); diff --git a/lib/EntsoePriceApi/include/EntsoeApi.h b/lib/EntsoePriceApi/include/EntsoeApi.h index 7ff12fba..af7aaff7 100644 --- a/lib/EntsoePriceApi/include/EntsoeApi.h +++ b/lib/EntsoePriceApi/include/EntsoeApi.h @@ -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, ...); diff --git a/lib/EntsoePriceApi/src/EntsoeApi.cpp b/lib/EntsoePriceApi/src/EntsoeApi.cpp index ce939c9c..2d64ae5a 100644 --- a/lib/EntsoePriceApi/src/EntsoeApi.cpp +++ b/lib/EntsoePriceApi/src/EntsoeApi.cpp @@ -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; } diff --git a/lib/HomeAssistantMqttHandler/src/HomeAssistantMqttHandler.cpp b/lib/HomeAssistantMqttHandler/src/HomeAssistantMqttHandler.cpp index 17066021..80ef0012 100644 --- a/lib/HomeAssistantMqttHandler/src/HomeAssistantMqttHandler.cpp +++ b/lib/HomeAssistantMqttHandler/src/HomeAssistantMqttHandler.cpp @@ -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); diff --git a/lib/JsonMqttHandler/src/JsonMqttHandler.cpp b/lib/JsonMqttHandler/src/JsonMqttHandler.cpp index 58bec458..273a331e 100644 --- a/lib/JsonMqttHandler/src/JsonMqttHandler.cpp +++ b/lib/JsonMqttHandler/src/JsonMqttHandler.cpp @@ -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); diff --git a/lib/RawMqttHandler/src/RawMqttHandler.cpp b/lib/RawMqttHandler/src/RawMqttHandler.cpp index ee8ae40c..35a516f8 100644 --- a/lib/RawMqttHandler/src/RawMqttHandler.cpp +++ b/lib/RawMqttHandler/src/RawMqttHandler.cpp @@ -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); diff --git a/lib/SvelteUi/app/src/App.svelte b/lib/SvelteUi/app/src/App.svelte index 785b1688..9433ffa5 100644 --- a/lib/SvelteUi/app/src/App.svelte +++ b/lib/SvelteUi/app/src/App.svelte @@ -33,7 +33,7 @@
- + diff --git a/lib/SvelteUi/app/src/lib/AmpPlot.svelte b/lib/SvelteUi/app/src/lib/AmpPlot.svelte index 40bec75a..3e5c9a75 100644 --- a/lib/SvelteUi/app/src/lib/AmpPlot.svelte +++ b/lib/SvelteUi/app/src/lib/AmpPlot.svelte @@ -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) }); diff --git a/lib/SvelteUi/app/src/lib/BarChart.svelte b/lib/SvelteUi/app/src/lib/BarChart.svelte index b6532b5a..e2ba9732 100644 --- a/lib/SvelteUi/app/src/lib/BarChart.svelte +++ b/lib/SvelteUi/app/src/lib/BarChart.svelte @@ -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 @@ {#each config.points as point, i} {#if point.value !== undefined} - + - {point.label} + {#if barWidth > 15} + {point.label} + {/if} {/if} {#if point.value2 > 0.0001} - - - {point.label2} + + {#if barWidth > 15} + {point.label2} + {/if} {/if} {/each} diff --git a/lib/SvelteUi/app/src/lib/ConfigurationPanel.svelte b/lib/SvelteUi/app/src/lib/ConfigurationPanel.svelte index a64f2afb..37e843fd 100644 --- a/lib/SvelteUi/app/src/lib/ConfigurationPanel.svelte +++ b/lib/SvelteUi/app/src/lib/ConfigurationPanel.svelte @@ -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 @@ {/if} +
+ User interface + +
+
+ Import gauge
+ +
+
+ Export gauge
+ +
+
+ Voltage
+ +
+
+ Amperage
+ +
+
+ Reactive
+ +
+
+ Realtime
+ +
+
+ Peaks
+ +
+
+ Price
+ +
+
+ Day plot
+ +
+
+ Month plot
+ +
+
+ Temperature plot
+ +
+
+
{#if sysinfo.board > 20 || sysinfo.chip == 'esp8266'}
Hardware diff --git a/lib/SvelteUi/app/src/lib/Dashboard.svelte b/lib/SvelteUi/app/src/lib/Dashboard.svelte index a27a8752..8cc35c26 100644 --- a/lib/SvelteUi/app/src/lib/Dashboard.svelte +++ b/lib/SvelteUi/app/src/lib/Dashboard.svelte @@ -1,6 +1,6 @@
+ {#if uiVisibility(sysinfo.ui.i, data.i)}
@@ -41,7 +43,8 @@
{data.ic ? data.ic.toFixed(1) : '-'} kWh
- {#if data.om || data.e > 0} + {/if} + {#if uiVisibility(sysinfo.ui.e, data.om || data.e > 0)}
@@ -52,39 +55,47 @@
{/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)}
{/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)}
{/if} + {#if uiVisibility(sysinfo.ui.r, data.ri > 0 || data.re > 0 || data.ric > 0 || data.rec > 0)}
+ {/if} + {#if uiVisibility(sysinfo.ui.c, data.ea)}
+ {/if} {#if data && data.pr && (data.pr.startsWith("10YNO") || data.pr == '10Y1001A1001A48H')}
{/if} - {#if (typeof data.p == "number") && !Number.isNaN(data.p)} + {#if uiVisibility(sysinfo.ui.p, (typeof data.p == "number") && !Number.isNaN(data.p))}
{/if} + {#if uiVisibility(sysinfo.ui.d, dayPlot)}
+ {/if} + {#if uiVisibility(sysinfo.ui.m, monthPlot)}
- {#if data.t && data.t != -127 && temperatures.c > 1} + {/if} + {#if uiVisibility(sysinfo.ui.s, data.t && data.t != -127 && temperatures.c > 1)}
diff --git a/lib/SvelteUi/app/src/lib/DataStores.js b/lib/SvelteUi/app/src/lib/DataStores.js index 7970faa6..32ea927e 100644 --- a/lib/SvelteUi/app/src/lib/DataStores.js +++ b/lib/SvelteUi/app/src/lib/DataStores.js @@ -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) => { diff --git a/lib/SvelteUi/app/src/lib/Header.svelte b/lib/SvelteUi/app/src/lib/Header.svelte index 1a701c58..95910656 100644 --- a/lib/SvelteUi/app/src/lib/Header.svelte +++ b/lib/SvelteUi/app/src/lib/Header.svelte @@ -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; diff --git a/lib/SvelteUi/app/src/lib/Helpers.js b/lib/SvelteUi/app/src/lib/Helpers.js index eb313ce5..36a9490c 100644 --- a/lib/SvelteUi/app/src/lib/Helpers.js +++ b/lib/SvelteUi/app/src/lib/Helpers.js @@ -143,4 +143,18 @@ export function priceError(err) { if(err < 0) return "Unspecified error "+err; return ""; -} \ No newline at end of file +} + +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); +} diff --git a/lib/SvelteUi/app/src/lib/PricePlot.svelte b/lib/SvelteUi/app/src/lib/PricePlot.svelte index 8d2509ef..c7d5d78a 100644 --- a/lib/SvelteUi/app/src/lib/PricePlot.svelte +++ b/lib/SvelteUi/app/src/lib/PricePlot.svelte @@ -76,8 +76,6 @@ }); } - console.log(yTicks); - config = { title: "Future energy price (" + json.currency + ")", padding: { top: 20, right: 15, bottom: 20, left: 35 }, diff --git a/lib/SvelteUi/app/src/lib/SetupPanel.svelte b/lib/SvelteUi/app/src/lib/SetupPanel.svelte index 250c7706..835af0d7 100644 --- a/lib/SvelteUi/app/src/lib/SetupPanel.svelte +++ b/lib/SvelteUi/app/src/lib/SetupPanel.svelte @@ -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; } diff --git a/lib/SvelteUi/app/src/lib/StatusPage.svelte b/lib/SvelteUi/app/src/lib/StatusPage.svelte index 02ea016d..dda8bd3a 100644 --- a/lib/SvelteUi/app/src/lib/StatusPage.svelte +++ b/lib/SvelteUi/app/src/lib/StatusPage.svelte @@ -1,5 +1,5 @@ -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} diff --git a/lib/SvelteUi/app/src/lib/VoltPlot.svelte b/lib/SvelteUi/app/src/lib/VoltPlot.svelte index 7751ade9..14ce6a01 100644 --- a/lib/SvelteUi/app/src/lib/VoltPlot.svelte +++ b/lib/SvelteUi/app/src/lib/VoltPlot.svelte @@ -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) }); diff --git a/lib/SvelteUi/app/vite.config.js b/lib/SvelteUi/app/vite.config.js index 295482b8..9bcb4a69 100644 --- a/lib/SvelteUi/app/vite.config.js +++ b/lib/SvelteUi/app/vite.config.js @@ -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", diff --git a/lib/SvelteUi/json/conf_ui.json b/lib/SvelteUi/json/conf_ui.json new file mode 100644 index 00000000..97fd05e3 --- /dev/null +++ b/lib/SvelteUi/json/conf_ui.json @@ -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 +}, \ No newline at end of file diff --git a/lib/SvelteUi/json/sysinfo.json b/lib/SvelteUi/json/sysinfo.json index 77c61290..f22661ee 100644 --- a/lib/SvelteUi/json/sysinfo.json +++ b/lib/SvelteUi/json/sysinfo.json @@ -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 } \ No newline at end of file diff --git a/lib/SvelteUi/src/AmsWebServer.cpp b/lib/SvelteUi/src/AmsWebServer.cpp index 1577cf09..7c2d0d98 100644 --- a/lib/SvelteUi/src/AmsWebServer.cpp +++ b/lib/SvelteUi/src/AmsWebServer.cpp @@ -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 +#include #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")); diff --git a/src/AmsToMqttBridge.ino b/src/AmsToMqttBridge.ino index 266abd7d..11e2c5d6 100644 --- a/src/AmsToMqttBridge.ino +++ b/src/AmsToMqttBridge.ino @@ -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;