Even more v2.2

This commit is contained in:
Gunnar Skjold 2022-11-21 18:48:04 +01:00
parent 8e54f23367
commit 902e43979b
23 changed files with 371 additions and 309 deletions

View File

@ -5,6 +5,7 @@
#define EEPROM_SIZE 1024*3
#define EEPROM_CHECK_SUM 100 // Used to check if config is stored. Change if structure changes
#define EEPROM_CLEARED_INDICATOR 0xFC
#define EEPROM_CONFIG_ADDRESS 0
#define EEPROM_TEMP_CONFIG_ADDRESS 2048

View File

@ -1,8 +1,9 @@
#include "AmsConfiguration.h"
bool AmsConfiguration::getSystemConfig(SystemConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.begin(EEPROM_SIZE);
uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
if(configVersion == EEPROM_CHECK_SUM || configVersion == EEPROM_CLEARED_INDICATOR) {
EEPROM.get(CONFIG_SYSTEM_START, config);
EEPROM.end();
return true;
@ -573,6 +574,15 @@ void AmsConfiguration::ackEnergyAccountingChange() {
void AmsConfiguration::clear() {
EEPROM.begin(EEPROM_SIZE);
SystemConfig sys;
EEPROM.get(CONFIG_SYSTEM_START, sys);
sys.userConfigured = false;
sys.dataCollectionConsent = 0;
strcpy(sys.country, "");
EEPROM.put(CONFIG_SYSTEM_START, sys);
MeterConfig meter;
clearMeter(meter);
EEPROM.put(CONFIG_METER_START, meter);
@ -605,7 +615,7 @@ void AmsConfiguration::clear() {
clearEnergyAccountingConfig(eac);
EEPROM.put(CONFIG_ENERGYACCOUNTING_START, eac);
EEPROM.put(EEPROM_CONFIG_ADDRESS, -1);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CLEARED_INDICATOR);
EEPROM.commit();
EEPROM.end();
}
@ -833,8 +843,8 @@ bool AmsConfiguration::relocateConfig96() {
SystemConfig sys;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_SYSTEM_START, sys);
sys.vendorConfigured = false;
sys.userConfigured = false;
sys.vendorConfigured = true;
sys.userConfigured = true;
sys.dataCollectionConsent = 0;
strcpy(sys.country, "");
EEPROM.put(CONFIG_SYSTEM_START, sys);

View File

@ -6,8 +6,8 @@
import Dashboard from './lib/Dashboard.svelte';
import ConfigurationPanel from './lib/ConfigurationPanel.svelte';
import StatusPage from './lib/StatusPage.svelte';
import VendorModal from './lib/VendorModal.svelte';
import SetupModal from './lib/SetupModal.svelte';
import VendorPanel from './lib/VendorPanel.svelte';
import SetupPanel from './lib/SetupPanel.svelte';
import Mask from './lib/Mask.svelte';
let sysinfo = {};
@ -25,7 +25,13 @@
<Router>
<Header data={data} sysinfo={sysinfo}/>
<Route path="/">
{#if sysinfo.vndcfg === false}
<VendorPanel sysinfo={sysinfo}/>
{:else if sysinfo.usrcfg === false}
<SetupPanel sysinfo={sysinfo}/>
{:else}
<Dashboard data={data}/>
{/if}
</Route>
<Route path="/configuration">
<ConfigurationPanel sysinfo={sysinfo}/>
@ -34,19 +40,10 @@
<StatusPage sysinfo={sysinfo} data={data}/>
</Route>
</Router>
{#if sysinfo.upgrading}
<Mask active=true message="Device is upgrading, please wait"/>
{:else if sysinfo.vndcfg === false}
{#if sysinfo.booting}
{:else if sysinfo.booting}
<Mask active=true message="Device is booting, please wait"/>
{:else}
<VendorModal sysinfo={sysinfo}/>
{/if}
{:else if sysinfo.usrcfg === false}
{#if sysinfo.booting}
<Mask active=true message="Device is booting, please wait"/>
{:else}
<SetupModal sysinfo={sysinfo}/>
{/if}
{/if}
</div>

View File

@ -40,8 +40,6 @@
});
}
config = {
height: 250,
width: 224,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: 0,

View File

@ -1,30 +1,37 @@
<script>
export let config;
let width;
let height;
let barWidth;
let xScale;
let yScale;
let heightAvailable;
$: {
let innerWidth = config.width - (config.padding.left + config.padding.right);
heightAvailable = height-(config.title ? 20 : 0);
let innerWidth = width - (config.padding.left + config.padding.right);
barWidth = innerWidth / config.points.length;
let yPerUnit = (config.height-config.padding.top-config.padding.bottom)/(config.y.max-config.y.min);
let yPerUnit = (heightAvailable-config.padding.top-config.padding.bottom)/(config.y.max-config.y.min);
xScale = function(i) {
return (i*barWidth)+config.padding.left;
};
yScale = function(i) {
if(!i) return config.height-config.padding.bottom;
if(i > config.y.max) return config.height;
let ret = config.height-config.padding.bottom-((i-config.y.min)*yPerUnit);
return ret > config.height || ret < 0 ? 0 : ret;
if(!i) return heightAvailable-config.padding.bottom;
if(i > config.y.max) return heightAvailable;
let ret = heightAvailable-config.padding.bottom-((i-config.y.min)*yPerUnit);
return ret > heightAvailable || ret < 0 ? 0 : ret;
};
};
</script>
<div class="chart" bind:clientWidth={config.width} bind:clientHeight={config.height}>
<svg height="{config.height}">
<div class="chart" bind:clientWidth={width} bind:clientHeight={height}>
{#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}
@ -38,7 +45,7 @@
<!-- x axis -->
<g class="axis x-axis">
{#each config.x.ticks as point, i}
<g class="tick" transform="translate({xScale(i)},{config.height})">
<g class="tick" transform="translate({xScale(i)},{heightAvailable})">
<text x="{barWidth/2}" y="-4">{point.label}</text>
</g>
{/each}
@ -60,7 +67,7 @@
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)+10})"
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>
{/each}
</g>
@ -70,6 +77,7 @@
<style>
.chart {
width: 100%;
height: 100%;
margin: 0 auto;
}

View File

@ -53,10 +53,24 @@
});
getConfiguration();
let isFactoryReset = false;
async function factoryReset() {
if(confirm("Are you sure you want to factory reset the device?")) {
const data = new URLSearchParams();
data.append("perform", "true");
const response = await fetch('/reset', {
method: 'POST',
body: data
});
let res = (await response.json());
isFactoryReset = res.success;
}
}
async function handleSubmit(e) {
loadingOrSaving = true;
const formData = new FormData(e.target)
const data = new URLSearchParams()
const formData = new FormData(e.target);
const data = new URLSearchParams();
for (let field of formData) {
const [key, value] = field
data.append(key, value)
@ -459,10 +473,6 @@
</label>
</div>
{/if}
<div class="bg-white m-2 p-2 rounded-md shadow-lg pb-4 text-gray-700">
<strong class="text-sm">Cloud</strong>
</div>
<div class="bg-white m-2 p-2 rounded-md shadow-lg pb-4 text-gray-700">
<strong class="text-sm">Hardware</strong>
{#if sysinfo.board > 20}
@ -557,6 +567,8 @@
{/if}
</div>
</div>
<button type="submit" class="font-bold py-2 px-4 rounded bg-blue-500 text-white float-right mr-3">Save</button>
<button type="button" on:click={factoryReset} class="font-bold py-2 px-4 rounded bg-red-500 text-white ml-2">Factory reset</button>
<button type="submit" class="font-bold py-2 px-4 rounded bg-blue-500 text-white float-right mr-2">Save</button>
</form>
<Mask active={loadingOrSaving} message="Loading configuration"/>
<Mask active={isFactoryReset} message="Device have been factory reset and switched to AP mode"/>

View File

@ -32,16 +32,20 @@
<div class="grid xl:grid-cols-6 lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2">
<div class="bg-white m-2 p-2 rounded shadow-lg">
<PowerGauge val={data.i ? data.i : 0} max={data.im} unit="W" label="Import"/>
<div class="grid grid-cols-2">
<div class="col-span-2">
<PowerGauge val={data.i ? data.i : 0} max={data.im} unit="W" label="Import"/>
</div>
<div>{data.mt ? metertype(data.mt) : '-'}</div>
<div class="text-right">{data.ic ? data.ic.toFixed(1) : '-'} kWh</div>
</div>
</div>
{#if data.om}
{#if data.om || data.e > 0}
<div class="bg-white m-2 p-2 rounded shadow-lg">
<PowerGauge val={data.e ? data.e : 0} max={data.om} unit="W" label="Export"/>
<div class="grid grid-cols-2">
<div class="col-span-2">
<PowerGauge val={data.e ? data.e : 0} max={data.om ? data.om : 10000} unit="W" label="Export"/>
</div>
<div></div>
<div class="text-right">{data.ec ? data.ec.toFixed(1) : '-'} kWh</div>
</div>
@ -60,18 +64,18 @@
<AccountingData data={data.ea} currency={prices.currency}/>
</div>
{#if prices.currency}
<div class="bg-white m-2 p-2 rounded shadow-lg xl:col-span-6 lg:col-span-3 md:col-span-3 sm:col-span-2">
<div class="bg-white m-2 p-2 rounded shadow-lg xl:col-span-6 lg:col-span-3 md:col-span-3 sm:col-span-2 h-64">
<PricePlot json={prices}/>
</div>
{/if}
<div class="bg-white m-2 p-2 rounded shadow-lg xl:col-span-6 lg:col-span-4 md:col-span-3 sm:col-span-2">
<div class="bg-white m-2 p-2 rounded shadow-lg xl:col-span-6 lg:col-span-4 md:col-span-3 sm:col-span-2 h-64">
<DayPlot json={dayPlot} />
</div>
<div class="bg-white m-2 p-2 rounded shadow-lg xl:col-span-6 lg:col-span-4 md:col-span-3 sm:col-span-2">
<div class="bg-white m-2 p-2 rounded shadow-lg 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}
<div class="bg-white m-2 p-2 rounded shadow-lg xl:col-span-6 lg:col-span-4 md:col-span-3 sm:col-span-2">
<div class="bg-white m-2 p-2 rounded shadow-lg xl:col-span-6 lg:col-span-4 md:col-span-3 sm:col-span-2 h-64">
<TemperaturePlot json={temperatures} />
</div>
{/if}

View File

@ -70,6 +70,7 @@
}
config = {
title: "Energy use last 24 hours (kWh)",
height: 226,
width: 1520,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
@ -86,7 +87,5 @@
};
</script>
<div class="mx-2">
<strong class="text-sm">Energy use last 24 hours (kWh)</strong>
<BarChart config={config} />
</div>
<BarChart config={config} />

View File

@ -55,7 +55,7 @@
<nav class="bg-violet-600 p-1 rounded-md mx-2">
<div class="flex flex-wrap space-x-4 text-sm text-gray-300">
<div class="flex-none text-lg text-gray-100 p-2">
<div class="flex text-lg text-gray-100 p-2">
<Link to="/">AMS reader <span>{sysinfo.version}</span></Link>
</div>
<div class="flex-none my-auto p-2 flex space-x-4">
@ -66,12 +66,12 @@
<div class="flex-none my-auto">Free mem: {data.m ? (data.m/1000).toFixed(1) : '-'}kb</div>
</div>
<div class="flex-auto my-auto justify-center p-2">
<Badge title="ESP" text={sysinfo.booting ? 'Booting' : data.v > 1.0 ? data.v.toFixed(2)+"V" : "ESP"} color={sysinfo.booting ? 'yellow' : data.em === 1 ? 'green' : data.em === 2 ? 'yellow' : data.em === 3 ? 'red' : 'gray'}/>
<Badge title="ESP" text={sysinfo.booting ? 'Booting' : data.v > 2.0 ? data.v.toFixed(2)+"V" : "ESP"} color={sysinfo.booting ? 'yellow' : data.em === 1 ? 'green' : data.em === 2 ? 'yellow' : data.em === 3 ? 'red' : 'gray'}/>
<Badge title="HAN" text="HAN" color={sysinfo.booting ? 'gray' : data.hm === 1 ? 'green' : data.hm === 2 ? 'yellow' : data.hm === 3 ? 'red' : 'gray'}/>
<Badge title="WiFi" text={data.r ? data.r.toFixed(0)+"dBm" : "WiFi"} color={sysinfo.booting ? 'gray' : data.wm === 1 ? 'green' : data.wm === 2 ? 'yellow' : data.wm === 3 ? 'red' : 'gray'}/>
<Badge title="MQTT" text="MQTT" color={sysinfo.booting ? 'gray' : data.mm === 1 ? 'green' : data.mm === 2 ? 'yellow' : data.mm === 3 ? 'red' : 'gray'}/>
</div>
<div class="flex-auto p-2 flex flex-row-reverse">
<div class="flex-auto p-2 flex flex-row-reverse flex-wrap">
<div class="flex-none">
<a class="float-right" href='https://github.com/gskjold/AmsToMqttBridge' target='_blank' rel="noreferrer" aria-label="GitHub"><img class="gh-logo" src={GitHubLogo} alt="GitHub repo"/></a>
</div>
@ -90,8 +90,8 @@
<div class="flex-none px-1 mt-1" title="Documentation">
<a href="https://github.com/gskjold/AmsToMqttBridge/wiki" target='_blank' rel="noreferrer"><HelpIcon/></a>
</div>
{#if sysinfo.fwconsent && nextVersion}
<div class="flex-none px-4 mt-1 text-yellow-500" title="New version: {nextVersion.tag_name}">
{#if sysinfo.fwconsent === 1 && nextVersion}
<div class="flex-none mr-3 text-yellow-500" title="New version: {nextVersion.tag_name}">
<button on:click={askUpgrade} class="flex"><DownloadIcon/> <span class="mt-1">{nextVersion.tag_name}</span></button>
</div>
{/if}

View File

@ -71,6 +71,7 @@
}
config = {
title: "Energy use last month (kWh)",
height: 226,
width: 1520,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
@ -87,7 +88,5 @@
};
</script>
<div class="mx-2">
<strong class="text-sm">Energy use last month (kWh)</strong>
<BarChart config={config} />
</div>
<BarChart config={config} />

View File

@ -31,7 +31,6 @@
}
.plot-value {
font-size: 1.7rem;
//cursor: pointer;
}
.plot-unit {
font-size: 1.0rem;

View File

@ -1,8 +1,6 @@
<script>
export let pct = 0;
export let color = "red";
let width = 300;
let height = 300;
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
@ -27,13 +25,7 @@
}
</script>
<div class="gauge" bind:clientWidth={width} bind:clientHeight={height}>
<svg height="100%"
width="100%"
viewBox="0 0 300 300"
xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg" height="100%">
<path d="{ describeArc(150, 150, 115, 210, 510) }" stroke="#eee" fill="none" stroke-width="55"/>
<path d="{ describeArc(150, 150, 115, 210, 210 + (300*pct/100)) }" stroke={color} fill="none" stroke-width="55"/>
</svg>
</div>
</svg>

View File

@ -63,8 +63,7 @@
}
config = {
height: 226,
width: 1520,
title: "Future energy price (" + json.currency + ")",
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,
@ -79,7 +78,5 @@
};
</script>
<div class="mx-2">
<strong class="text-sm">Future energy price ({json.currency})</strong>
<BarChart config={config} />
</div>
<BarChart config={config} />

View File

@ -1,124 +0,0 @@
<script>
import { sysinfoStore } from './DataStores.js';
import Mask from './Mask.svelte'
export let sysinfo = {}
let staticIp = false;
let loadingOrSaving = false;
let tries = 0;
function scanForDevice() {
var url = "";
tries++;
if(sysinfo.net.ip && tries%3 == 0) {
url = "http://" + sysinfo.net.ip;
} else if(sysinfo.hostname && tries%3 == 1) {
url = "http://" + sysinfo.hostname;
} else if(sysinfo.hostname && tries%3 == 2) {
url = "http://" + sysinfo.hostname + ".local";
} else {
url = "";
}
if(console) console.log("Trying url " + url);
var retry = function() {
setTimeout(scanForDevice, 1000);
};
var xhr = new XMLHttpRequest();
xhr.timeout = 5000;
xhr.addEventListener('abort', retry);
xhr.addEventListener('error', retry);
xhr.addEventListener('timeout', retry);
xhr.addEventListener('load', function(e) {
window.location.href = url ? url : "/";
});
xhr.open("GET", url + "/is-alive", true);
xhr.send();
};
async function handleSubmit(e) {
loadingOrSaving = true;
const formData = new FormData(e.target)
const data = new URLSearchParams()
for (let field of formData) {
const [key, value] = field
data.append(key, value)
}
const response = await fetch('/save', {
method: 'POST',
body: data
});
let res = (await response.json())
loadingOrSaving = false;
sysinfoStore.update(s => {
s.usrcfg = res.success;
s.booting = res.reboot;
setTimeout(scanForDevice, 5000);
return s;
});
}
</script>
<div class="z-10" aria-modal="true">
<div class="fixed inset-0 bg-gray-500 bg-opacity-50 flex items-center justify-center">
<div class="bg-white m-2 p-3 rounded-md shadow-lg pb-4 text-gray-700 w-96">
<form on:submit|preventDefault={handleSubmit}>
<input type="hidden" name="s" value="true"/>
<strong class="text-sm">Setup</strong>
<div class="my-3">
SSID<br/>
<input name="ss" type="text" class="h-10 rounded-md shadow-sm border-gray-300 w-full"/>
</div>
<div class="my-3">
PSK<br/>
<input name="sp" type="password" class="h-10 rounded-md shadow-sm border-gray-300 w-full"/>
</div>
<div>
Hostname:
<input name="sh" bind:value={sysinfo.hostname} type="text" class="h-10 rounded-md shadow-sm border-gray-300 w-full" maxlength="32" pattern="[a-z0-9_-]+" placeholder="Optional, ex.: ams-reader"/>
</div>
<div class="my-3">
<label><input type="checkbox" name="sm" value="static" class="rounded mb-1" bind:checked={staticIp} /> Static IP</label>
{#if staticIp}
<br/>
<div class="flex">
<input name="si" type="text" class="h-10 rounded-l-md shadow-sm border-gray-300 w-full" required={staticIp}/>
<select name="su" class="h-10 rounded-r-md shadow-sm border-l-0 border-gray-300" required={staticIp}>
<option value="255.255.255.0">/24</option>
<option value="255.255.0.0">/16</option>
<option value="255.0.0.0">/8</option>
</select>
</div>
{/if}
</div>
{#if staticIp}
<div class="my-3 flex">
<div>
Gateway<br/>
<input name="sg" type="text" class="h-10 rounded-l-md shadow-sm border-gray-300 w-full"/>
</div>
<div>
DNS<br/>
<input name="sd" type="text" class="h-10 rounded-r-md border-l-0 shadow-sm border-gray-300 w-full"/>
</div>
</div>
{/if}
<div class="my-3">
<label><input type="checkbox" name="sf" value="true" class="rounded mb-1"/> Enable OTA upgrade (implies data collection)</label><br/>
<a href="https://github.com/gskjold/AmsToMqttBridge/wiki/Data-collection-on-OTA-firmware-upgrade" target="_blank" class="text-blue-600 hover:text-blue-800">Read more</a>
</div>
<div class="my-3">
<button type="submit" class="font-bold py-1 px-4 rounded bg-blue-500 text-white float-right">Save</button>
</div>
</form>
</div>
</div>
</div>
<Mask active={loadingOrSaving} message="Saving your configuration to the device"/>

View File

@ -0,0 +1,122 @@
<script>
import { sysinfoStore } from './DataStores.js';
import Mask from './Mask.svelte'
export let sysinfo = {}
let staticIp = false;
let loadingOrSaving = false;
let tries = 0;
function scanForDevice() {
var url = "";
tries++;
if(sysinfo.net.ip && tries%3 == 0) {
url = "http://" + sysinfo.net.ip;
} else if(sysinfo.hostname && tries%3 == 1) {
url = "http://" + sysinfo.hostname;
} else if(sysinfo.hostname && tries%3 == 2) {
url = "http://" + sysinfo.hostname + ".local";
} else {
url = "";
}
if(console) console.log("Trying url " + url);
var retry = function() {
setTimeout(scanForDevice, 1000);
};
var xhr = new XMLHttpRequest();
xhr.timeout = 5000;
xhr.addEventListener('abort', retry);
xhr.addEventListener('error', retry);
xhr.addEventListener('timeout', retry);
xhr.addEventListener('load', function(e) {
window.location.href = url ? url : "/";
});
xhr.open("GET", url + "/is-alive", true);
xhr.send();
};
async function handleSubmit(e) {
loadingOrSaving = true;
const formData = new FormData(e.target)
const data = new URLSearchParams()
for (let field of formData) {
const [key, value] = field
data.append(key, value)
}
const response = await fetch('/save', {
method: 'POST',
body: data
});
let res = (await response.json())
loadingOrSaving = false;
sysinfoStore.update(s => {
s.usrcfg = res.success;
s.booting = res.reboot;
setTimeout(scanForDevice, 5000);
return s;
});
}
</script>
<div class="grid xl:grid-cols-4 lg:grid-cols-3 md:grid-cols-2">
<div class="bg-white m-2 p-3 rounded-md shadow-lg pb-4 text-gray-700">
<form on:submit|preventDefault={handleSubmit}>
<input type="hidden" name="s" value="true"/>
<strong class="text-sm">Setup</strong>
<div class="my-3">
SSID<br/>
<input name="ss" type="text" class="h-10 rounded-md shadow-sm border-gray-300 w-full"/>
</div>
<div class="my-3">
PSK<br/>
<input name="sp" type="password" class="h-10 rounded-md shadow-sm border-gray-300 w-full"/>
</div>
<div>
Hostname:
<input name="sh" bind:value={sysinfo.hostname} type="text" class="h-10 rounded-md shadow-sm border-gray-300 w-full" maxlength="32" pattern="[a-z0-9_-]+" placeholder="Optional, ex.: ams-reader"/>
</div>
<div class="my-3">
<label><input type="checkbox" name="sm" value="static" class="rounded mb-1" bind:checked={staticIp} /> Static IP</label>
{#if staticIp}
<br/>
<div class="flex">
<input name="si" type="text" class="h-10 rounded-l-md shadow-sm border-gray-300 w-full" required={staticIp}/>
<select name="su" class="h-10 rounded-r-md shadow-sm border-l-0 border-gray-300" required={staticIp}>
<option value="255.255.255.0">/24</option>
<option value="255.255.0.0">/16</option>
<option value="255.0.0.0">/8</option>
</select>
</div>
{/if}
</div>
{#if staticIp}
<div class="my-3 flex">
<div>
Gateway<br/>
<input name="sg" type="text" class="h-10 rounded-l-md shadow-sm border-gray-300 w-full"/>
</div>
<div>
DNS<br/>
<input name="sd" type="text" class="h-10 rounded-r-md border-l-0 shadow-sm border-gray-300 w-full"/>
</div>
</div>
{/if}
<div class="my-3">
<label><input type="checkbox" name="sf" value="true" class="rounded mb-1"/> Enable OTA upgrade (implies data collection)</label><br/>
<a href="https://github.com/gskjold/AmsToMqttBridge/wiki/Data-collection-on-OTA-firmware-upgrade" target="_blank" class="text-blue-600 hover:text-blue-800">Read more</a>
</div>
<div class="my-3">
<button type="submit" class="font-bold py-1 px-4 rounded bg-blue-500 text-white float-right">Save</button>
</div>
</form>
</div>
</div>
<Mask active={loadingOrSaving} message="Saving your configuration to the device"/>

View File

@ -81,13 +81,18 @@
</div>
<div class="my-2 flex">
Latest version:
{#if sysinfo.fwconsent && nextVersion && nextVersion.tag_name}
<a href={nextVersion.html_url} class="ml-2 text-blue-600 hover:text-blue-800" target='_blank' rel="noreferrer">{nextVersion.tag_name}</a>
{#if sysinfo.fwconsent === 1 && nextVersion && nextVersion.tag_name}
<div class="flex-none ml-2 text-green-500" title="Install this version">
<button on:click={askUpgrade}><DownloadIcon/></button>
</div>
{/if}
</div>
{#if sysinfo.fwconsent === 2}
<div class="my-2">
<div class="my-auto bg-yellow-500 text-yellow-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">You have not consented to OTA firmware upgrade, link to self-upgrade is disabled</div>
</div>
{/if}
{#if sysinfo.board == 2 || sysinfo.board == 4 || sysinfo.board == 7 }
<div class="my-auto bg-red-500 text-red-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">
{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.

View File

@ -50,6 +50,7 @@
}
config = {
title: "Temperature sensors (°C)",
height: 226,
width: 1520,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
@ -66,7 +67,5 @@
};
</script>
<div class="mx-2">
<strong class="text-sm">Temperature sensors (&deg;C)</strong>
<BarChart config={config} />
</div>
<BarChart config={config} />

View File

@ -1,61 +0,0 @@
<script>
import { sysinfoStore } from './DataStores.js';
import BoardTypeSelectOptions from './BoardTypeSelectOptions.svelte';
import UartSelectOptions from './UartSelectOptions.svelte';
import Mask from './Mask.svelte'
export let sysinfo = {}
let loadingOrSaving = false;
async function handleSubmit(e) {
loadingOrSaving = true;
const formData = new FormData(e.target)
const data = new URLSearchParams()
for (let field of formData) {
const [key, value] = field
data.append(key, value)
}
const response = await fetch('/save', {
method: 'POST',
body: data
});
let res = (await response.json())
loadingOrSaving = false;
sysinfoStore.update(s => {
s.vndcfg = res.success;
s.booting = res.reboot;
return s;
});
}
</script>
<div class="z-10" aria-modal="true">
<div class="fixed inset-0 bg-gray-500 bg-opacity-50 flex items-center justify-center">
<div class="bg-white m-2 p-3 rounded-md shadow-lg pb-4 text-gray-700 w-96">
<form on:submit|preventDefault={handleSubmit}>
<input type="hidden" name="v" value="true"/>
<strong class="text-sm">Vendor configuration</strong>
<div class="my-3">
Board type<br/>
<select name="vb" bind:value={sysinfo.board} class="h-10 rounded-md shadow-sm border-gray-300 p-0 w-full">
<BoardTypeSelectOptions chip={sysinfo.chip}/>
</select>
</div>
{#if sysinfo.board && sysinfo.board > 20}
<div class="my-3">
HAN GPIO<br/>
<select name="vh" class="h-10 rounded-md shadow-sm border-gray-300">
<UartSelectOptions chip={sysinfo.chip}/>
</select>
</div>
{/if}
<div class="my-3">
<button type="submit" class="font-bold py-1 px-4 rounded bg-blue-500 text-white float-right">Save</button>
</div>
</form>
</div>
</div>
</div>
<Mask active={loadingOrSaving} message="Saving device configuration" />

View File

@ -0,0 +1,60 @@
<script>
import { sysinfoStore } from './DataStores.js';
import BoardTypeSelectOptions from './BoardTypeSelectOptions.svelte';
import UartSelectOptions from './UartSelectOptions.svelte';
import Mask from './Mask.svelte'
export let sysinfo = {}
let loadingOrSaving = false;
async function handleSubmit(e) {
loadingOrSaving = true;
const formData = new FormData(e.target)
const data = new URLSearchParams()
for (let field of formData) {
const [key, value] = field
data.append(key, value)
}
const response = await fetch('/save', {
method: 'POST',
body: data
});
let res = (await response.json())
loadingOrSaving = false;
sysinfoStore.update(s => {
s.vndcfg = res.success;
s.booting = res.reboot;
return s;
});
}
</script>
<div class="grid xl:grid-cols-4 lg:grid-cols-3 md:grid-cols-2">
<div class="bg-white m-2 p-3 rounded-md shadow-lg pb-4 text-gray-700">
<form on:submit|preventDefault={handleSubmit}>
<input type="hidden" name="v" value="true"/>
<strong class="text-sm">Vendor configuration</strong>
<div class="my-3">
Board type<br/>
<select name="vb" bind:value={sysinfo.board} class="h-10 rounded-md shadow-sm border-gray-300 p-0 w-full">
<BoardTypeSelectOptions chip={sysinfo.chip}/>
</select>
</div>
{#if sysinfo.board && sysinfo.board > 20}
<div class="my-3">
HAN GPIO<br/>
<select name="vh" class="h-10 rounded-md shadow-sm border-gray-300">
<UartSelectOptions chip={sysinfo.chip}/>
</select>
</div>
{/if}
<div class="my-3">
<button type="submit" class="font-bold py-1 px-4 rounded bg-blue-500 text-white float-right">Save</button>
</div>
<span class="clear-both">&nbsp;</span>
</form>
</div>
</div>
<Mask active={loadingOrSaving} message="Saving device configuration" />

View File

@ -39,8 +39,6 @@
});
}
config = {
height: 250,
width: 224,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,

View File

@ -17,7 +17,7 @@ export default defineConfig({
plugins: [svelte()],
server: {
proxy: {
"/data.json": "http://192.168.233.229",
"/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",

View File

@ -94,6 +94,8 @@ private:
HTTPUpload& uploadFile(const char* path);
void factoryResetPost();
void notFound();
};

View File

@ -58,6 +58,8 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, Meter
server.on(F("/upgrade"), HTTP_POST, std::bind(&AmsWebServer::upgrade, this));
server.on(F("/firmware"), HTTP_POST, std::bind(&AmsWebServer::firmwarePost, this), std::bind(&AmsWebServer::firmwareUpload, this));
server.on(F("/is-alive"), HTTP_GET, std::bind(&AmsWebServer::isAliveCheck, this));
server.on(F("/reset"), HTTP_POST, std::bind(&AmsWebServer::factoryResetPost, this));
server.onNotFound(std::bind(&AmsWebServer::notFound, this));
@ -226,7 +228,7 @@ void AmsWebServer::dataJson() {
uint8_t espStatus;
#if defined(ESP8266)
if(vcc == 0) {
if(vcc < 2.0) { // Voltage not correct, ESP would not run on this voltage
espStatus = 1;
} else if(vcc > 3.1 && vcc < 3.5) {
espStatus = 1;
@ -236,7 +238,7 @@ void AmsWebServer::dataJson() {
espStatus = 3;
}
#elif defined(ESP32)
if(vcc == 0) {
if(vcc < 2.0) { // Voltage not correct, ESP would not run on this voltage
espStatus = 1;
} else if(vcc > 2.8 && vcc < 3.5) {
espStatus = 1;
@ -1010,6 +1012,8 @@ void AmsWebServer::handleSave() {
//TODO sys.country
sys.dataCollectionConsent = server.hasArg(F("sf")) && server.arg(F("sf")) == F("true") ? 1 : 2;
config->setSystemConfig(sys);
performRestart = true;
}
if(server.hasArg(F("m")) && server.arg(F("m")) == F("true")) {
@ -1305,58 +1309,64 @@ void AmsWebServer::reboot() {
void AmsWebServer::upgrade() {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /upgrade over http...\n");
SystemConfig sys;
config->getSystemConfig(sys);
DynamicJsonDocument doc(128);
doc["reboot"] = true;
doc["success"] = sys.dataCollectionConsent == 1;
doc["reboot"] = sys.dataCollectionConsent == 1;
serializeJson(doc, buf, BufferSize);
server.send(200, MIME_JSON, buf);
server.handleClient();
delay(250);
if(sys.dataCollectionConsent == 1) {
server.handleClient();
delay(250);
String customFirmwareUrl = "";
if(server.hasArg(F("url"))) {
customFirmwareUrl = server.arg(F("url"));
}
String customFirmwareUrl = "";
if(server.hasArg(F("url"))) {
customFirmwareUrl = server.arg(F("url"));
}
String url = customFirmwareUrl.isEmpty() || !customFirmwareUrl.startsWith(F("http")) ? F("http://ams2mqtt.rewiredinvent.no/hub/firmware/update") : customFirmwareUrl;
String url = customFirmwareUrl.isEmpty() || !customFirmwareUrl.startsWith(F("http")) ? F("http://ams2mqtt.rewiredinvent.no/hub/firmware/update") : customFirmwareUrl;
if(server.hasArg(F("version"))) {
url += "/" + server.arg(F("version"));
}
if(server.hasArg(F("version"))) {
url += "/" + server.arg(F("version"));
}
WiFiClient client;
#if defined(ESP8266)
String chipType = F("esp8266");
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
String chipType = F("esp32s2");
#elif defined(ESP32)
#if defined(CONFIG_FREERTOS_UNICORE)
String chipType = F("esp32solo");
#else
String chipType = F("esp32");
WiFiClient client;
#if defined(ESP8266)
String chipType = F("esp8266");
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
String chipType = F("esp32s2");
#elif defined(ESP32)
#if defined(CONFIG_FREERTOS_UNICORE)
String chipType = F("esp32solo");
#else
String chipType = F("esp32");
#endif
#endif
#endif
#if defined(ESP8266)
ESPhttpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
t_httpUpdate_return ret = ESPhttpUpdate.update(client, url, VERSION);
#elif defined(ESP32)
HTTPUpdate httpUpdate;
httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
HTTPUpdateResult ret = httpUpdate.update(client, url, String(VERSION) + "-" + chipType);
#endif
#if defined(ESP8266)
ESPhttpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
t_httpUpdate_return ret = ESPhttpUpdate.update(client, url, VERSION);
#elif defined(ESP32)
HTTPUpdate httpUpdate;
httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
HTTPUpdateResult ret = httpUpdate.update(client, url, String(VERSION) + "-" + chipType);
#endif
switch(ret) {
case HTTP_UPDATE_FAILED:
debugger->printf(PSTR("Update failed"));
break;
case HTTP_UPDATE_NO_UPDATES:
debugger->printf(PSTR("No Update"));
break;
case HTTP_UPDATE_OK:
debugger->printf(PSTR("Update OK"));
break;
switch(ret) {
case HTTP_UPDATE_FAILED:
debugger->printf(PSTR("Update failed"));
break;
case HTTP_UPDATE_NO_UPDATES:
debugger->printf(PSTR("No Update"));
break;
case HTTP_UPDATE_OK:
debugger->printf(PSTR("Update OK"));
break;
}
}
}
@ -1457,3 +1467,38 @@ void AmsWebServer::isAliveCheck() {
server.sendHeader(F("Access-Control-Allow-Origin"), F("*"));
server.send(200);
}
void AmsWebServer::factoryResetPost() {
if(!checkSecurity(1))
return;
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Performing factory reset"));
bool success = false;
if(server.hasArg(F("perform")) && server.arg(F("perform")) == F("true")) {
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Formatting LittleFS"));
LittleFS.format();
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Clearing configuration"));
config->clear();
success = true;
}
DynamicJsonDocument doc(128);
doc["success"] = success;
doc["reboot"] = success;
serializeJson(doc, buf, BufferSize);
server.send(200, MIME_JSON, buf);
server.handleClient();
delay(250);
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf(PSTR("Rebooting"));
delay(1000);
#if defined(ESP8266)
ESP.reset();
#elif defined(ESP32)
ESP.restart();
#endif
}