Compare commits

..

6 Commits

Author SHA1 Message Date
Gunnar Skjold
4d128700c1 Added UI readme 2026-03-05 16:40:05 +01:00
Gunnar Skjold
6743750d8f Made proxy target configurable 2026-03-05 16:39:20 +01:00
Gunnar Skjold
640e957065 Optimizing footprint 2026-03-05 16:34:10 +01:00
Gunnar Skjold
d4f11c0412 Updated node version in workflows 2026-03-05 16:22:40 +01:00
Gunnar Skjold
01acc6d6e8 Consolidated new routes and old components 2026-03-05 16:22:19 +01:00
Gunnar Skjold
e89bb53941 Initial changes to migrate to Svelte 5 2026-03-05 15:51:06 +01:00
35 changed files with 1888 additions and 1912 deletions

View File

@@ -51,7 +51,7 @@ jobs:
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: '19.x'
node-version: '22.x'
- name: Build with node
run: |
cd lib/SvelteUi/app

View File

@@ -73,7 +73,7 @@ jobs:
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: '19.x'
node-version: '22.x'
- name: Build with node
run: |
cd lib/SvelteUi/app

View File

@@ -19,7 +19,6 @@ bool WiFiAccessPointConnectionHandler::connect(NetworkConfig config, SystemConfi
//wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, 0); // Disable default gw
WiFi.mode(WIFI_AP);
WiFi.persistent(false);
WiFi.softAP(config.ssid, config.psk);
dnsServer.setErrorReplyCode(DNSReplyCode::NoError);

View File

@@ -100,7 +100,6 @@ bool WiFiClientConnectionHandler::connect(NetworkConfig config, SystemConfig sys
}
#endif
WiFi.setAutoReconnect(true);
WiFi.persistent(false);
this->config = config;
#if defined(ESP32)
if(begin(config.ssid, config.psk)) {

View File

@@ -0,0 +1,59 @@
# SvelteUi App
Web interface for AMS Reader firmware built with Svelte 5 and Vite 6.
## Development Setup
### Prerequisites
- Node.js 20.x or 22.x LTS (required for Vite 6)
- npm
### Local Development Configuration
To develop against your AMS reader device, you need to configure the proxy target:
1. Copy the example config file:
```bash
cp vite.config.local.example.js vite.config.local.js
```
2. Edit `vite.config.local.js` and update the IP address to match your device:
```javascript
export default {
proxyTarget: "http://192.168.1.100" // Your device's IP
}
```
3. The `vite.config.local.js` file is gitignored, so your personal settings won't be committed.
### Running Development Server
```bash
npm install
npm run dev
```
The dev server will proxy API requests to your configured device IP.
### Building for Production
```bash
npm run build
```
The build output will be in the `dist/` directory.
## Project Structure
- `src/` - Application source code
- `routes/` - Page components using svelte-spa-router
- `lib/` - Shared components and utilities
- `public/` - Static assets (favicon, etc.)
- `dist/` - Build output (not committed to git)
## Key Technologies
- **Svelte 5.17.0** - UI framework
- **Vite 6.0.7** - Build tool
- **svelte-spa-router 4.0.1** - Hash-based routing
- **Tailwind CSS** - Styling

File diff suppressed because one or more lines are too long

View File

@@ -8,10 +8,9 @@
<link rel="mask-icon" href="/favicon.svg" color="#000000">
<title>AMS reader</title>
<script type="module" crossorigin src="/index.js"></script>
<link rel="stylesheet" href="/index.css">
<link rel="stylesheet" crossorigin href="/index.css">
</head>
<body class="bg-gray-100 dark:bg-gray-900">
<div id="app"></div>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -9,28 +9,22 @@
"build": "vite build",
"preview": "vite preview"
},
"overrides": {
"svelte-navigator": {
"svelte": ">=4.x"
}
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.1.0",
"@sveltejs/vite-plugin-svelte": "^5.0.2",
"@tailwindcss/forms": "^0.5.3",
"autoprefixer": "^10.4.14",
"http-proxy-middleware": "^2.0.9",
"postcss": "^8.4.31",
"postcss-load-config": "^4.0.1",
"svelte": "^4.2.19",
"svelte-navigator": "^3.2.2",
"svelte-preprocess": "^5.0.3",
"svelte": "^5.17.0",
"svelte-spa-router": "^4.0.1",
"svelte-preprocess": "^6.0.3",
"svelte-qrcode": "^1.0.0",
"tailwindcss": "^3.3.1",
"vite": "^4.5.14"
"vite": "^6.0.7"
},
"dependencies": {
"cssnano": "^5.1.15",
"esbuild": ">=0.25.0",
"ipaddr.js": "^2.3.0"
"esbuild": ">=0.25.0"
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<title>Amsleser</title>
<g transform="translate(-29.5,-83)">
<circle r="4.8016944" cy="123.56455" cx="55.064552"
style="fill:none;stroke:#045c7c;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 41.298717,103.9049 a 24,24 0 0 1 27.531669,0"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 35.562952,95.713384 a 34,34 0 0 1 39.003199,-2e-6"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 47.034482,112.09642 a 14,14 0 0 1 16.06014,0"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle r="3" cy="105.99158" cx="38.181862"
style="fill:none;stroke:#045c7c;stroke-width:2.4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle r="3" cy="97.959579" cx="77.491386"
style="fill:none;stroke:#045c7c;stroke-width:2.4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,49 +1,27 @@
<script>
import { Router, Route, navigate } from "svelte-navigator";
import { getTariff, tariffStore, sysinfoStore, dataStore, importPricesStore, exportPricesStore, dayPlotStore, monthPlotStore, temperaturesStore, getSysinfo } from './lib/DataStores.js';
import Router from "svelte-spa-router";
import { push } from "svelte-spa-router";
import { getTariff, sysinfoStore, dataStore, getSysinfo } from './lib/DataStores.js';
import { translationsStore, getTranslations } from "./lib/TranslationService.js";
import Favicon from './assets/favicon.svg'; // Need this for the build
import Header from './lib/Header.svelte';
import Dashboard from './lib/Dashboard.svelte';
import ConfigurationPanel from './lib/ConfigurationPanel.svelte';
import StatusPage from './lib/StatusPage.svelte';
import VendorPanel from './lib/VendorPanel.svelte';
import SetupPanel from './lib/SetupPanel.svelte';
import DashboardRoute from './routes/DashboardRoute.svelte';
import ConfigurationRoute from './routes/ConfigurationRoute.svelte';
import StatusRoute from './routes/StatusRoute.svelte';
import PriceConfigRoute from './routes/PriceConfigRoute.svelte';
import MqttCaRoute from './routes/MqttCaRoute.svelte';
import MqttCertRoute from './routes/MqttCertRoute.svelte';
import MqttKeyRoute from './routes/MqttKeyRoute.svelte';
import ConsentRoute from './routes/ConsentRoute.svelte';
import SetupRoute from './routes/SetupRoute.svelte';
import VendorRoute from './routes/VendorRoute.svelte';
import EditDayRoute from './routes/EditDayRoute.svelte';
import EditMonthRoute from './routes/EditMonthRoute.svelte';
import Mask from './lib/Mask.svelte';
import FileUploadComponent from "./lib/FileUploadComponent.svelte";
import ConsentComponent from "./lib/ConsentComponent.svelte";
import PriceConfig from "./lib/PriceConfig.svelte";
import DataEdit from "./lib/DataEdit.svelte";
import { updateRealtime } from "./lib/RealtimeStore.js";
let basepath = document.getElementsByTagName('base')[0].getAttribute("href");
if(!basepath) basepath = "/";
let importPrices;
importPricesStore.subscribe(update => {
importPrices = update;
});
let exportPrices;
exportPricesStore.subscribe(update => {
exportPrices = update;
});
let dayPlot;
dayPlotStore.subscribe(update => {
dayPlot = update;
});
let monthPlot;
monthPlotStore.subscribe(update => {
monthPlot = update;
});
let temperatures;
temperaturesStore.subscribe(update => {
temperatures = update;
});
let translations = {};
translationsStore.subscribe(update => {
translations = update;
@@ -56,11 +34,11 @@
sysinfoStore.subscribe(update => {
sysinfo = update;
if(sysinfo.vndcfg === false) {
navigate(basepath + "vendor");
push("/vendor");
} else if(sysinfo.usrcfg === false) {
navigate(basepath + "setup");
push("/setup");
} else if(sysinfo.fwconsent === 0) {
navigate(basepath + "consent");
push("/consent");
}
if(sysinfo.ui.k === 1) {
@@ -94,53 +72,26 @@
updateRealtime(update);
});
let tariffData = {};
tariffStore.subscribe(update => {
tariffData = update;
});
getTariff();
</script>
<div class="container mx-auto m-3">
<Router basepath={basepath}>
<Header data={data} basepath={basepath}/>
<Route path="/">
<Dashboard data={data} sysinfo={sysinfo} importPrices={importPrices} exportPrices={exportPrices} dayPlot={dayPlot} monthPlot={monthPlot} temperatures={temperatures} translations={translations} tariffData={tariffData}/>
</Route>
<Route path="/configuration">
<ConfigurationPanel sysinfo={sysinfo} basepath={basepath} data={data}/>
</Route>
<Route path="/priceconfig">
<PriceConfig basepath={basepath}/>
</Route>
<Route path="/status">
<StatusPage sysinfo={sysinfo} data={data}/>
</Route>
<Route path="/mqtt-ca">
<FileUploadComponent title="CA" action="/mqtt-ca"/>
</Route>
<Route path="/mqtt-cert">
<FileUploadComponent title="certificate" action="/mqtt-cert"/>
</Route>
<Route path="/mqtt-key">
<FileUploadComponent title="private key" action="/mqtt-key"/>
</Route>
<Route path="/consent">
<ConsentComponent sysinfo={sysinfo} basepath={basepath}/>
</Route>
<Route path="/setup">
<SetupPanel sysinfo={sysinfo}/>
</Route>
<Route path="/vendor">
<VendorPanel sysinfo={sysinfo} basepath={basepath}/>
</Route>
<Route path="/edit-day">
<DataEdit prefix="UTC Hour" data={dayPlot} url="/dayplot" basepath={basepath}/>
</Route>
<Route path="/edit-month">
<DataEdit prefix="Day" data={monthPlot} url="/monthplot" basepath={basepath}/>
</Route>
</Router>
<Header data={data} basepath={basepath}/>
<Router routes={{
'/': DashboardRoute,
'/configuration': ConfigurationRoute,
'/priceconfig': PriceConfigRoute,
'/status': StatusRoute,
'/mqtt-ca': MqttCaRoute,
'/mqtt-cert': MqttCertRoute,
'/mqtt-key': MqttKeyRoute,
'/consent': ConsentRoute,
'/setup': SetupRoute,
'/vendor': VendorRoute,
'/edit-day': EditDayRoute,
'/edit-month': EditMonthRoute,
}} />
{#if sysinfo.booting}
{#if sysinfo.trying}

View File

@@ -206,8 +206,4 @@ svg {
border-width: 9px;
border-style: solid;
border-color: #ddd transparent transparent transparent;
}
.link {
@apply cursor-pointer;
}

View File

@@ -1,5 +1,4 @@
<script>
import { Link } from "svelte-navigator";
import { tooltip } from './tooltip';
export let config;
@@ -47,7 +46,7 @@
{#if config.link}
<div class="text-xs text-right">
{#if config.link.route}
<Link to={config.link.url}>{config.link.text}</Link>
<a href={"#" + config.link.url}>{config.link.text}</a>
{:else}
<a href={config.link.url} target={config.link.target}>{config.link.text}</a>
{/if}

View File

@@ -1,6 +1,6 @@
<script>
import { translationsStore } from './TranslationService';
import { navigate } from 'svelte-navigator';
import { push } from 'svelte-spa-router';
import Mask from './Mask.svelte'
export let prefix;
@@ -59,7 +59,7 @@
let res = (await response.json())
saving = false;
navigate(basepath);
push(basepath);
}
</script>
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
@@ -70,7 +70,7 @@
{#each importElements as el}
<label class="flex w-60 m-1">
<span class="in-pre">{el.name}</span>
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.01" class="in-txt w-full text-right"/>
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.001" class="in-txt w-full text-right"/>
<span class="in-post">kWh</span>
</label>
{/each}
@@ -82,7 +82,7 @@
{#each exportElements as el}
<label class="flex w-60 m-1">
<span class="in-pre">{el.name}</span>
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.01" class="in-txt w-full text-right"/>
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.001" class="in-txt w-full text-right"/>
<span class="in-post">kWh</span>
</label>
{/each}

View File

@@ -1,5 +1,4 @@
<script>
import { Link } from "svelte-navigator";
import { sysinfoStore } from './DataStores.js';
import { upgrade, upgradeWarningText } from './UpgradeHelper';
import { boardtype, isBusPowered, wiki, bcol } from './Helpers.js';
@@ -46,7 +45,7 @@
<nav class="hdr">
<div class="flex flex-wrap space-x-4 text-sm text-gray-300">
<div class="flex text-lg text-gray-100 p-2">
<Link to="/">AMS reader <span>{sysinfo.version}</span></Link>
<a href={basepath}>AMS reader <span>{sysinfo.version}</span></a>
</div>
<div class="flex-none my-auto p-2 flex space-x-4">
<div class="flex-none my-auto"><Uptime epoch={data.u}/></div>
@@ -79,10 +78,10 @@
</div>
{#if sysinfo.vndcfg && sysinfo.usrcfg}
<div class="flex-none px-1 mt-1" title={translations.header?.config ?? ""}>
<Link to="/configuration"><GearIcon/></Link>
<a href="#/configuration"><GearIcon/></a>
</div>
<div class="flex-none px-1 mt-1" title={translations.header?.status ?? ""}>
<Link to="/status"><InfoIcon/></Link>
<a href="#/status"><InfoIcon/></a>
</div>
{/if}
<div class="flex-none px-1 mt-1" title={translations.header?.doc ?? ""}>

View File

@@ -41,22 +41,22 @@
for(i = 0; i < tariffData.p.length; i++) {
let peak = tariffData.p[i];
let title = "";
let peakTitle = "";
let daylabel = "-";
if(peak.d > 0) {
daylabel = zeropad(peak.d) + ".";
title = zeropad(peak.d) + "." + (translations.months ? translations.months?.[new Date().getMonth()] : zeropad(new Date().getMonth()+1));
peakTitle = zeropad(peak.d) + "." + (translations.months ? translations.months?.[new Date().getMonth()] : zeropad(new Date().getMonth()+1));
if(tariffData.p.length < 4) {
daylabel = title;
daylabel = peakTitle;
}
}
if(!isNaN(peak.h))
title = title + ' ' + zeropad(peak.h) + ':00';
title = title + ': ' + peak.v.toFixed(2) + ' kWh';
peakTitle = peakTitle + ' ' + zeropad(peak.h) + ':00';
peakTitle = peakTitle + ': ' + peak.v.toFixed(2) + ' kWh';
points.push({
label: peak.v.toFixed(2),
value: peak.v,
title: title,
title: peakTitle,
color: dark ? '#5c2da5' : '#7c3aed'
});
xTicks.push({

View File

@@ -1,7 +1,8 @@
import "./app.postcss";
import { mount } from "svelte";
import App from "./App.svelte";
const app = new App({
const app = mount(App, {
target: document.getElementById("app"),
});

View File

@@ -1,22 +1,24 @@
<script>
import { getConfiguration, configurationStore } from './ConfigurationStore'
import { sysinfoStore, networksStore } from './DataStores.js';
import fetchWithTimeout from './fetchWithTimeout';
import { translationsStore } from './TranslationService';
import { wiki, ipPattern, asciiPattern, asciiPatternExt, charAndNumPattern, hexPattern, numPattern, isBusPowered } from './Helpers.js';
import UartSelectOptions from './UartSelectOptions.svelte';
import Mask from './Mask.svelte'
import Badge from './Badge.svelte';
import CountrySelectOptions from './CountrySelectOptions.svelte';
import { Link, navigate } from 'svelte-navigator';
import SubnetOptions from './SubnetOptions.svelte';
import { getConfiguration, configurationStore } from '../lib/ConfigurationStore'
import { sysinfoStore, networksStore, dataStore } from '../lib/DataStores.js';
import fetchWithTimeout from '../lib/fetchWithTimeout';
import { translationsStore } from '../lib/TranslationService';
import { wiki, ipPattern, asciiPattern, asciiPatternExt, charAndNumPattern, hexPattern, numPattern, isBusPowered } from '../lib/Helpers.js';
import UartSelectOptions from '../lib/UartSelectOptions.svelte';
import Mask from '../lib/Mask.svelte'
import Badge from '../lib/Badge.svelte';
import CountrySelectOptions from '../lib/CountrySelectOptions.svelte';
import { push } from 'svelte-spa-router';
import SubnetOptions from '../lib/SubnetOptions.svelte';
import QrCode from 'svelte-qrcode';
export let basepath = "/";
export let sysinfo = {};
export let data;
let basepath = "/";
let sysinfo = {};
let data;
let form;
sysinfoStore.subscribe(v => sysinfo = v);
dataStore.subscribe(v => data = v);
let translations = {};
translationsStore.subscribe(update => {
translations = update;
@@ -151,7 +153,7 @@
});
saving = false;
navigate(basepath);
push(basepath);
}
async function reboot() {
@@ -249,34 +251,9 @@
_global.bindToCloud = function() {
console.log("BIND CALLED");
}
async function toggleShowWifiPass() {
const input = form.querySelector('input[name="wp"]');
toggleShowPass.call(this, input);
}
async function toggleShowMqttPass() {
const input = form.querySelector('input[name="qa"]');
toggleShowPass.call(this, input);
}
async function toggleShowWebPass() {
const input = form.querySelector('input[name="gp"]');
toggleShowPass.call(this, input);
}
async function toggleShowPass(input) {
if(input.type === 'password') {
input.type = 'text';
this.textContent = '🙈';
} else {
input.type = 'password';
this.textContent = '👁️';
}
}
</script>
<form bind:this={form} on:submit|preventDefault={handleSubmit} autocomplete="off">
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
{#if configuration?.g}
<div class="cnt">
@@ -362,7 +339,7 @@
</div>
</div>
<div class="my-1">
<Link to="/priceconfig" class="text-blue-600 hover:text-blue-800">{translations.conf?.price?.conf ?? "Configure"}</Link>
<a href="#/priceconfig" class="text-blue-600 hover:text-blue-800">{translations.conf?.price?.conf ?? "Configure"}</a>
</div>
<div class="my-1">
<label><input type="checkbox" name="pe" value="true" bind:checked={configuration.p.e} class="rounded mb-1"/> {translations.conf?.price?.enabled ?? "Enabled"}</label>
@@ -386,10 +363,7 @@
</div>
<div class="my-1">
{translations.conf?.general?.security?.password ?? "Password"}<br/>
<div class="flex">
<input name="gp" bind:value={configuration.g.p} type="password" class="in-f w-full" maxlength="36" pattern={asciiPattern}/>
<span on:click={toggleShowWebPass} class="in-post link">👁️</span>
</div>
<input name="gp" bind:value={configuration.g.p} type="password" class="in-s" maxlength="36" pattern={asciiPattern}/>
</div>
{/if}
<div class="my-1">
@@ -541,10 +515,7 @@
</div>
<div class="my-1">
{translations.conf?.connection?.psk ?? "Password"}<br/>
<div class="flex">
<input name="wp" bind:value={configuration.w.p} type="password" class="in-f w-full" pattern={asciiPatternExt}/>
<span on:click={toggleShowWifiPass} class="in-post link">👁️</span>
</div>
<input name="wp" bind:value={configuration.w.p} type="password" class="in-s" pattern={asciiPatternExt}/>
</div>
<div class="my-1 flex">
<div class="w-1/2">
@@ -635,28 +606,28 @@
<div class="my-1 flex">
<span class="flex pr-2">
{#if configuration.q.s.c}
<span class="bd-on"><Link to="/mqtt-ca">{translations.conf?.mqtt?.ca_ok ?? "CA OK"}</Link></span>
<span class="bd-on"><a href="#/mqtt-ca">{translations.conf?.mqtt?.ca_ok ?? "CA OK"}</a></span>
<span class="bd-off" on:click={askDeleteCa} on:keypress={askDeleteCa}>&#128465;</span>
{:else}
<Link to="/mqtt-ca"><Badge color="blue" text={translations.conf?.mqtt?.btn_ca_upload ?? "Upload CA"} title={translations.conf?.mqtt?.title_ca ?? ""}/></Link>
<a href="#/mqtt-ca"><Badge color="blue" text={translations.conf?.mqtt?.btn_ca_upload ?? "Upload CA"} title={translations.conf?.mqtt?.title_ca ?? ""}/></a>
{/if}
</span>
<span class="flex pr-2">
{#if configuration.q.s.r}
<span class="bd-on"><Link to="/mqtt-cert">{translations.conf?.mqtt?.crt_ok ?? "Cert OK"}</Link></span>
<span class="bd-on"><a href="#/mqtt-cert">{translations.conf?.mqtt?.crt_ok ?? "Cert OK"}</a></span>
<span class="bd-off" on:click={askDeleteCert} on:keypress={askDeleteCert}>&#128465;</span>
{:else}
<Link to="/mqtt-cert"><Badge color="blue" text={translations.conf?.mqtt?.btn_crt_upload ?? "Upload cert"} title={translations.conf?.mqtt?.title_crt ?? ""}/></Link>
<a href="#/mqtt-cert"><Badge color="blue" text={translations.conf?.mqtt?.btn_crt_upload ?? "Upload cert"} title={translations.conf?.mqtt?.title_crt ?? ""}/></a>
{/if}
</span>
<span class="flex pr-2">
{#if configuration.q.s.k}
<span class="bd-on"><Link to="/mqtt-key">{translations.conf?.mqtt?.key_ok ?? "Key OK"}</Link></span>
<span class="bd-on"><a href="#/mqtt-key">{translations.conf?.mqtt?.key_ok ?? "Key OK"}</a></span>
<span class="bd-off" on:click={askDeleteKey} on:keypress={askDeleteKey}>&#128465;</span>
{:else}
<Link to="/mqtt-key"><Badge color="blue" text={translations.conf?.mqtt?.btn_key_upload ?? "Upload key"} title={translations.conf?.mqtt?.title_key ?? ""}/></Link>
<a href="#/mqtt-key"><Badge color="blue" text={translations.conf?.mqtt?.btn_key_upload ?? "Upload key"} title={translations.conf?.mqtt?.title_key ?? ""}/></a>
{/if}
</span>
</div>
@@ -667,10 +638,7 @@
</div>
<div class="my-1">
{translations.conf?.mqtt?.pass ?? "Password"}<br/>
<div class="flex">
<input name="qa" bind:value={configuration.q.a} type="password" class="in-f w-full" pattern={asciiPatternExt}/>
<span on:click={toggleShowMqttPass} class="in-post link">👁️</span>
</div>
<input name="qa" bind:value={configuration.q.a} type="password" class="in-s" pattern={asciiPatternExt}/>
</div>
<div class="my-1 flex">
<div>

View File

@@ -1,18 +1,19 @@
<script>
import { sysinfoStore } from './DataStores.js';
import { translationsStore } from './TranslationService.js';
import Mask from './Mask.svelte'
import { navigate } from 'svelte-navigator';
import { wiki } from './Helpers';
import { sysinfoStore } from '../lib/DataStores.js';
import { translationsStore } from '../lib/TranslationService.js';
import Mask from '../lib/Mask.svelte'
import { push } from 'svelte-spa-router';
export let basepath = "/";
export let sysinfo = {};
let basepath = "/";
let sysinfo = {};
let translations = {};
translationsStore.subscribe(update => {
translations = update;
});
sysinfoStore.subscribe(v => sysinfo = v);
let loadingOrSaving = false;
async function handleSubmit(e) {
@@ -36,7 +37,7 @@
s.booting = res.reboot;
return s;
});
navigate(basepath);
push(basepath);
}
</script>

View File

@@ -1,26 +1,38 @@
<script>
import { ampcol, exportcol, metertype, uiVisibility, formatUnit, fmtnum, formatCurrency } from './Helpers.js';
import PowerGauge from './PowerGauge.svelte';
import VoltPlot from './VoltPlot.svelte';
import ReactiveData from './ReactiveData.svelte';
import AccountingData from './AccountingData.svelte';
import PricePlot from './PricePlot.svelte';
import DayPlot from './DayPlot.svelte';
import MonthPlot from './MonthPlot.svelte';
import TemperaturePlot from './TemperaturePlot.svelte';
import TariffPeakChart from './TariffPeakChart.svelte';
import RealtimePlot from './RealtimePlot.svelte';
import PerPhasePlot from './PerPhasePlot.svelte';
import { ampcol, exportcol, metertype, uiVisibility, formatUnit, fmtnum, formatCurrency } from '../lib/Helpers.js';
import PowerGauge from '../lib/PowerGauge.svelte';
import VoltPlot from '../lib/VoltPlot.svelte';
import ReactiveData from '../lib/ReactiveData.svelte';
import AccountingData from '../lib/AccountingData.svelte';
import PricePlot from '../lib/PricePlot.svelte';
import DayPlot from '../lib/DayPlot.svelte';
import MonthPlot from '../lib/MonthPlot.svelte';
import TemperaturePlot from '../lib/TemperaturePlot.svelte';
import TariffPeakChart from '../lib/TariffPeakChart.svelte';
import RealtimePlot from '../lib/RealtimePlot.svelte';
import PerPhasePlot from '../lib/PerPhasePlot.svelte';
import { dataStore, sysinfoStore, importPricesStore, exportPricesStore, dayPlotStore, monthPlotStore, temperaturesStore, tariffStore } from '../lib/DataStores.js';
import { translationsStore } from '../lib/TranslationService.js';
export let data = {}
export let sysinfo = {}
export let importPrices = {}
export let exportPrices = {}
export let dayPlot = {}
export let monthPlot = {}
export let temperatures = {};
export let translations = {};
export let tariffData = {};
let data = {}
let sysinfo = {}
let importPrices = {}
let exportPrices = {}
let dayPlot = {}
let monthPlot = {}
let temperatures = {};
let translations = {};
let tariffData = {};
dataStore.subscribe(v => data = v);
sysinfoStore.subscribe(v => sysinfo = v);
importPricesStore.subscribe(v => importPrices = v);
exportPricesStore.subscribe(v => exportPrices = v);
dayPlotStore.subscribe(v => dayPlot = v);
monthPlotStore.subscribe(v => monthPlot = v);
temperaturesStore.subscribe(v => temperatures = v);
translationsStore.subscribe(v => translations = v);
tariffStore.subscribe(v => tariffData = v);
let it,et,threePhase, l1e, l2e, l3e;
$: {

View File

@@ -0,0 +1,11 @@
<script>
import DataEdit from '../lib/DataEdit.svelte';
import { dayPlotStore } from '../lib/DataStores.js';
let basepath = "/";
let dayPlot;
dayPlotStore.subscribe(v => dayPlot = v);
</script>
<DataEdit prefix="UTC Hour" data={dayPlot} url="/dayplot" {basepath} />

View File

@@ -0,0 +1,11 @@
<script>
import DataEdit from '../lib/DataEdit.svelte';
import { monthPlotStore } from '../lib/DataStores.js';
let basepath = "/";
let monthPlot;
monthPlotStore.subscribe(v => monthPlot = v);
</script>
<DataEdit prefix="Day" data={monthPlot} url="/monthplot" {basepath} />

View File

@@ -1,9 +1,6 @@
<script>
import Mask from "./Mask.svelte";
import { translationsStore } from "./TranslationService";
export let action;
export let title;
import Mask from "../lib/Mask.svelte";
import { translationsStore } from "../lib/TranslationService";
let translations = {};
translationsStore.subscribe(update => {
@@ -15,12 +12,12 @@
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
<div class="cnt">
<strong>{translations.upload?.title ?? "Upload"} {title}</strong>
<strong>{translations.upload?.title ?? "Upload"} CA</strong>
<p class="mb-4">{translations.upload?.desc ?? ""}</p>
<form action="{action}" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
<form action="/mqtt-ca" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
<input name="file" type="file">
<div class="w-full text-right mt-4">
<button type="submit" class="btn-pri"><p class="mb-4">{translations.btn?.upload ?? "Upload"}</button>
<button type="submit" class="btn-pri">{translations.btn?.upload ?? "Upload"}</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,25 @@
<script>
import Mask from "../lib/Mask.svelte";
import { translationsStore } from "../lib/TranslationService";
let translations = {};
translationsStore.subscribe(update => {
translations = update;
});
let uploading = false;
</script>
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
<div class="cnt">
<strong>{translations.upload?.title ?? "Upload"} certificate</strong>
<p class="mb-4">{translations.upload?.desc ?? ""}</p>
<form action="/mqtt-cert" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
<input name="file" type="file">
<div class="w-full text-right mt-4">
<button type="submit" class="btn-pri">{translations.btn?.upload ?? "Upload"}</button>
</div>
</form>
</div>
</div>
<Mask active={uploading} message={translations.upload?.mask ?? "Uploading"}/>

View File

@@ -0,0 +1,25 @@
<script>
import Mask from "../lib/Mask.svelte";
import { translationsStore } from "../lib/TranslationService";
let translations = {};
translationsStore.subscribe(update => {
translations = update;
});
let uploading = false;
</script>
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
<div class="cnt">
<strong>{translations.upload?.title ?? "Upload"} private key</strong>
<p class="mb-4">{translations.upload?.desc ?? ""}</p>
<form action="/mqtt-key" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
<input name="file" type="file">
<div class="w-full text-right mt-4">
<button type="submit" class="btn-pri">{translations.btn?.upload ?? "Upload"}</button>
</div>
</form>
</div>
</div>
<Mask active={uploading} message={translations.upload?.mask ?? "Uploading"}/>

View File

@@ -1,11 +1,9 @@
<script>
import { priceConfigStore, getPriceConfig } from './ConfigurationStore'
import { translationsStore } from './TranslationService';
import { wiki, zeropad } from './Helpers.js';
import Mask from './Mask.svelte'
import { navigate } from 'svelte-navigator';
export let basepath = "/";
import { priceConfigStore, getPriceConfig } from '../lib/ConfigurationStore'
import { translationsStore } from '../lib/TranslationService';
import { wiki, zeropad } from '../lib/Helpers.js';
import Mask from '../lib/Mask.svelte'
import { push } from 'svelte-spa-router';
let translations = {};
translationsStore.subscribe(update => {
@@ -53,7 +51,7 @@
let res = (await response.json())
saving = false;
navigate(basepath + "configuration");
push("/configuration");
}
let toggleDay = function(arr, day) {

View File

@@ -1,34 +1,27 @@
<script>
import { sysinfoStore, networksStore } from './DataStores.js';
import { translationsStore } from './TranslationService.js';
import Mask from './Mask.svelte'
import SubnetOptions from './SubnetOptions.svelte';
import { scanForDevice, charAndNumPattern, asciiPatternExt, ipPattern } from './Helpers.js';
import { sysinfoStore, networksStore } from '../lib/DataStores.js';
import { translationsStore } from '../lib/TranslationService.js';
import Mask from '../lib/Mask.svelte'
import SubnetOptions from '../lib/SubnetOptions.svelte';
import { scanForDevice, charAndNumPattern, asciiPatternExt, ipPattern } from '../lib/Helpers.js';
let translations = {};
translationsStore.subscribe(update => {
translations = update;
});
let form;
let ssid = '';
let psk = '';
let manual = false;
let networks = {};
networksStore.subscribe(update => {
networks = update;
manual = update?.c == 0;
ssid = update?.n[0]?.s ?? '';
});
export let sysinfo = {}
let sysinfo = {}
sysinfoStore.subscribe(v => sysinfo = v);
let staticIp = false;
let connectionMode = 1;
let loadingOrSaving = false;
let wifiTestInProgress = false;
let wifiTestOk = false;
let wifiTestError = 0;
function updateSysinfo(url) {
sysinfoStore.update(s => {
@@ -37,9 +30,9 @@
});
}
async function handleSubmit() {
async function handleSubmit(e) {
loadingOrSaving = true;
const formData = new FormData(form);
const formData = new FormData(e.target);
const data = new URLSearchParams();
for (let field of formData) {
const [key, value] = field;
@@ -67,71 +60,17 @@
return s;
});
}
async function wifiTest() {
let response;
if(wifiTestInProgress) {
response = await fetch('wifitest.json');
} else {
wifiTestInProgress = true;
wifiTestOk = false;
const data = new URLSearchParams();
data.append('ssid', ssid);
data.append('psk', psk);
response = await fetch('wifitest.json', {
method: 'POST',
body: data
});
}
const res = await response.json();
if(res?.time == 0) {
wifiTestInProgress = false;
wifiTestOk = res.status == 3;
wifiTestError = res.status;
if(wifiTestOk) {
sysinfoStore.update(s => {
s.net.ip = res.ip;
return s;
});
setTimeout(handleSubmit, 1000);
}
} else if(wifiTestInProgress) {
if(res.time > 30000) {
wifiTestError = 4;
wifiTestInProgress = false;
} else {
setTimeout(wifiTest, 2000);
}
}
}
async function resetWifiTest() {
wifiTestInProgress = false;
wifiTestOk = false;
wifiTestError = 0;
}
async function toggleShowPass() {
const input = form.querySelector('input[name="sp"]');
if(input.type === 'password') {
input.type = 'text';
this.textContent = '🙈';
} else {
input.type = 'password';
this.textContent = '👁️';
}
}
</script>
<div class="grid xl:grid-cols-4 lg:grid-cols-3 md:grid-cols-2">
<div class="cnt">
<form bind:this={form} on:submit|preventDefault={handleSubmit}>
<form on:submit|preventDefault={handleSubmit}>
<input type="hidden" name="s" value="true"/>
<strong class="text-sm">{translations.setup?.title ?? "Setup"}</strong>
<div class="my-3">
{translations.conf?.connection?.title ?? "Connection"}<br/>
<select name="sc" class="in-s" bind:value={connectionMode} on:input={resetWifiTest}>
<select name="sc" class="in-s" bind:value={connectionMode}>
<option value={1}>{translations.conf?.connection?.wifi ?? "Connect to WiFi"}</option>
<option value={2}>{translations.conf?.connection?.ap ?? "Standalone access point"}</option>
{#if sysinfo.if && sysinfo.if.eth}
@@ -145,9 +84,9 @@
<label class="float-right mr-3"><input type="checkbox" value="true" bind:checked={manual} class="rounded mb-1"/> manual</label>
<br/>
{#if manual}
<input name="ss" bind:value={ssid} on:input={resetWifiTest} type="text" pattern={asciiPatternExt} class="in-s" required={connectionMode == 1 || connectionMode == 2}/>
<input name="ss" type="text" pattern={asciiPatternExt} class="in-s" required={connectionMode == 1 || connectionMode == 2}/>
{:else}
<select name="ss" bind:value={ssid} on:change={resetWifiTest} class="in-s" required={connectionMode == 1 || connectionMode == 2}>
<select name="ss" class="in-s" required={connectionMode == 1 || connectionMode == 2}>
{#if networks?.c == -1}
<option value="" selected disabled>Scanning...</option>
{/if}
@@ -161,10 +100,7 @@
</div>
<div class="my-3">
{translations.conf?.connection?.psk ?? "Password"}<br/>
<div class="flex">
<input name="sp" bind:value={psk} on:input={resetWifiTest} type="password" pattern={asciiPatternExt} class="in-f w-full" autocomplete="off" required={connectionMode == 2}/>
<span on:click={toggleShowPass} class="in-post link">👁️</span>
</div>
<input name="sp" type="password" pattern={asciiPatternExt} class="in-s" autocomplete="off" required={connectionMode == 2}/>
</div>
{/if}
<div>
@@ -196,21 +132,7 @@
</div>
{/if}
<div class="my-3">
{#if connectionMode != 1}
<button type="submit" class="btn-pri">{translations.btn?.save ?? "Save"}</button>
{:else if wifiTestOk}
<div class="bd-green">{translations.setup?.testok ?? "Connection successful (" + sysinfo.net.ip + ")"}</div>
<button type="submit" class="btn-pri">{translations.btn?.save ?? "Save"}</button>
{:else if wifiTestInProgress}
<div class="bd-yellow">{translations.setup?.testconn ?? "Testing connection"}</div>
{:else}
{#if wifiTestError}
<div class="bd-red">{ (translations.setup?.testfail ?? "Connection failed") + ': ' + (translations.errors?.wifi?.[wifiTestError] ?? wifiTestError) }</div>
<button type="submit" class="btn-pri">{translations.btn?.forcesave ?? "Force save"}</button>
{:else}
<button type="button" class="btn-pri" on:click={wifiTest}>{translations.btn?.save ?? "Save"}</button>
{/if}
{/if}
<button type="submit" class="btn-pri">{translations.btn?.save ?? "Save"}</button>
</div>
</form>
</div>

View File

@@ -1,16 +1,70 @@
<script>
import { metertype, boardtype, isBusPowered, getBaseChip, wiki } from './Helpers.js';
import { getSysinfo, sysinfoStore } from './DataStores.js';
import { upgrade, upgradeWarningText } from './UpgradeHelper';
import { translationsStore } from './TranslationService.js';
import { Link } from 'svelte-navigator';
import Clock from './Clock.svelte';
import Mask from './Mask.svelte';
import { scanForDevice } from './Helpers.js';
import ipaddr from 'ipaddr.js';
import { metertype, boardtype, isBusPowered, getBaseChip, wiki } from '../lib/Helpers.js';
import { getSysinfo, sysinfoStore, dataStore } from '../lib/DataStores.js';
import { upgrade, upgradeWarningText } from '../lib/UpgradeHelper';
import { translationsStore } from '../lib/TranslationService.js';
import Clock from '../lib/Clock.svelte';
import Mask from '../lib/Mask.svelte';
import { scanForDevice } from '../lib/Helpers.js';
export let data;
export let sysinfo;
let data;
let sysinfo;
dataStore.subscribe(v => data = v);
sysinfoStore.subscribe(v => sysinfo = v);
// Format IPv6 address to compact form (RFC 5952)
const formatIPv6 = (addr) => {
if (!addr) return addr;
// Split into groups
const groups = addr.toLowerCase().split(':');
// Remove leading zeros from each group
const normalized = groups.map(g => g.replace(/^0+/, '') || '0');
// Find longest sequence of consecutive zeros
let maxStart = -1, maxLen = 0;
let currStart = -1, currLen = 0;
for (let i = 0; i < normalized.length; i++) {
if (normalized[i] === '0') {
if (currStart === -1) currStart = i;
currLen++;
} else {
if (currLen > maxLen) {
maxStart = currStart;
maxLen = currLen;
}
currStart = -1;
currLen = 0;
}
}
// Check final sequence
if (currLen > maxLen) {
maxStart = currStart;
maxLen = currLen;
}
// Only compress if we have 2 or more consecutive zeros
if (maxLen > 1) {
const before = normalized.slice(0, maxStart);
const after = normalized.slice(maxStart + maxLen);
if (before.length === 0 && after.length === 0) {
return '::';
} else if (before.length === 0) {
return '::' + after.join(':');
} else if (after.length === 0) {
return before.join(':') + '::';
} else {
return before.join(':') + '::' + after.join(':');
}
}
return normalized.join(':');
};
let cfgItems = [{
name: 'WiFi',
@@ -73,11 +127,11 @@
}
let firmwareFileInput;
let firmwareFiles = [];
let firmwareFiles = null;
let firmwareUploading = false;
let configFileInput;
let configFiles = [];
let configFiles = null;
let configUploading = false;
getSysinfo();
@@ -119,7 +173,7 @@
};
$: {
if(configFiles.length == 1) {
if(configFiles && configFiles.length == 1) {
let file = configFiles[0];
let reader = new FileReader();
let parseConfigFile = ( e ) => {
@@ -146,7 +200,7 @@
{translations.status?.device?.chip ?? "Chip"}: {sysinfo.chip} {#if sysinfo.cpu}({sysinfo.cpu}MHz){/if}
</div>
<div class="my-2">
{translations.status?.device?.device ?? "Device"}: <Link to="/vendor">{boardtype(sysinfo.chip, sysinfo.board)}</Link>
{translations.status?.device?.device ?? "Device"}: <a href="#/vendor">{boardtype(sysinfo.chip, sysinfo.board)}</a>
</div>
<div class="my-2">
{translations.status?.device?.mac ?? "MAC"}: {sysinfo.mac}
@@ -169,9 +223,9 @@
{/if}
{#if data?.a}
<div class="my-2">
<Link to="/consent">
<a href="#/consent">
<span class="btn-pri-sm">{translations.status?.device?.btn_consents ?? "Consents"}</span>
</Link>
</a>
<button on:click={askReboot} class="btn-yellow-sm float-right">{translations.btn?.reboot ?? "Reboot"}</button>
</div>
{/if}
@@ -208,11 +262,11 @@
</div>
{#if sysinfo.net.ipv6}
<div class="my-2">
IPv6: <span style="font-size: 14px;">{ipaddr.parse(sysinfo.net.ipv6)}</span>
IPv6: <span style="font-size: 14px;">{formatIPv6(sysinfo.net.ipv6)}</span>
</div>
<div class="my-2">
{#if sysinfo.net.dns1v6}DNSv6: <span style="font-size: 14px;">{ipaddr.parse(sysinfo.net.dns1v6)}</span>{/if}
{#if sysinfo.net.dns2v6}DNSv6: <span style="font-size: 14px;">{ipaddr.parse(sysinfo.net.dns2v6)}</span>{/if}
{#if sysinfo.net.dns1v6}DNSv6: <span style="font-size: 14px;">{formatIPv6(sysinfo.net.dns1v6)}</span>{/if}
{#if sysinfo.net.dns2v6}DNSv6: <span style="font-size: 14px;">{formatIPv6(sysinfo.net.dns2v6)}</span>{/if}
</div>
{/if}
</div>
@@ -267,7 +321,7 @@
<div class="my-2 flex">
<form action="firmware" enctype="multipart/form-data" method="post" on:submit={() => firmwareUploading=true} autocomplete="off">
<input style="display:none" name="file" type="file" accept=".bin" bind:this={firmwareFileInput} bind:files={firmwareFiles}>
{#if firmwareFiles.length == 0}
{#if !firmwareFiles || firmwareFiles.length == 0}
<button type="button" on:click={()=>{firmwareFileInput.click();}} class="btn-pri-sm float-right">{translations.status?.firmware?.btn_select_file ?? "Select file"}</button>
{:else}
{firmwareFiles[0].name}
@@ -287,13 +341,13 @@
{/each}
<label class="my-1 mx-3 col-span-2"><input type="checkbox" class="rounded" name="ic" value="true"/> {translations.status?.backup?.secrets ?? "Include secrets"}<br/><small>{translations.status?.backup?.secrets_desc ?? ""}</small></label>
</div>
{#if configFiles.length == 0}
{#if !configFiles || configFiles.length == 0}
<button type="submit" class="btn-pri-sm float-right">{translations.status?.backup?.btn_download ?? "Download"}</button>
{/if}
</form>
<form on:submit|preventDefault={uploadConfigFile} autocomplete="off">
<input style="display:none" name="file" type="file" accept=".cfg" bind:this={configFileInput} bind:files={configFiles}>
{#if configFiles.length == 0}
{#if !configFiles || configFiles.length == 0}
<button type="button" on:click={()=>{configFileInput.click();}} class="btn-pri-sm">{translations.status?.backup?.btn_select_file ?? "Select file"}</button>
{:else}
{configFiles[0].name}

View File

@@ -1,12 +1,11 @@
<script>
import { sysinfoStore } from './DataStores.js';
import BoardTypeSelectOptions from './BoardTypeSelectOptions.svelte';
import UartSelectOptions from './UartSelectOptions.svelte';
import Mask from './Mask.svelte'
import { navigate } from 'svelte-navigator';
import { sysinfoStore } from '../lib/DataStores.js';
import BoardTypeSelectOptions from '../lib/BoardTypeSelectOptions.svelte';
import UartSelectOptions from '../lib/UartSelectOptions.svelte';
import Mask from '../lib/Mask.svelte'
import { push } from 'svelte-spa-router';
export let basepath = "/";
export let sysinfo = {};
let sysinfo = {};
let loadingOrSaving = false;
async function handleSubmit(e) {
@@ -32,7 +31,7 @@
return s;
});
navigate(basepath + (sysinfo.usrcfg ? "" : "setup"));
push(sysinfo.usrcfg ? "/" : "/setup");
}
let cc = true;

View File

@@ -1,46 +1,62 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// Try to import local config, fall back to default if not found
let localConfig = { proxyTarget: "http://192.168.4.1" };
try {
const imported = await import('./vite.config.local.js');
localConfig = imported.default;
} catch (e) {
console.log('No vite.config.local.js found, using default proxy target:', localConfig.proxyTarget);
console.log('Copy vite.config.local.example.js to vite.config.local.js to customize');
}
// https://vitejs.dev/config/
export default defineConfig({
build: {
outDir: 'dist',
assetsDir: '.',
minify: 'esbuild',
target: 'es2020',
rollupOptions: {
output: {
assetFileNames: '[name][extname]',
chunkFileNames: '[name].js',
entryFileNames: '[name].js'
entryFileNames: '[name].js',
manualChunks: undefined
}
}
},
plugins: [svelte()],
plugins: [svelte({
compilerOptions: {
dev: false
}
})],
server: {
proxy: {
"/data.json": "http://192.168.4.1",
"/energyprice.json": "http://192.168.4.1",
"/importprice.json": "http://192.168.4.1",
"/exportprice.json": "http://192.168.4.1",
"/dayplot.json": "http://192.168.4.1",
"/monthplot.json": "http://192.168.4.1",
"/temperature.json": "http://192.168.4.1",
"/sysinfo.json": "http://192.168.4.1",
"/configuration.json": "http://192.168.4.1",
"/tariff.json": "http://192.168.4.1",
"/realtime.json": "http://192.168.4.1",
"/priceconfig.json": "http://192.168.4.1",
"/translations.json": "http://192.168.4.1",
"/cloudkey.json": "http://192.168.4.1",
"/wifiscan.json": "http://192.168.4.1",
"/wifitest.json": "http://192.168.4.1",
"/save": "http://192.168.4.1",
"/reboot": "http://192.168.4.1",
"/configfile": "http://192.168.4.1",
"/upgrade": "http://192.168.4.1",
"/mqtt-ca": "http://192.168.4.1",
"/mqtt-cert": "http://192.168.4.1",
"/mqtt-key": "http://192.168.4.1",
"/logo.svg": "http://192.168.4.1",
"/data.json": localConfig.proxyTarget,
"/energyprice.json": localConfig.proxyTarget,
"/importprice.json": localConfig.proxyTarget,
"/exportprice.json": localConfig.proxyTarget,
"/dayplot.json": localConfig.proxyTarget,
"/monthplot.json": localConfig.proxyTarget,
"/temperature.json": localConfig.proxyTarget,
"/sysinfo.json": localConfig.proxyTarget,
"/configuration.json": localConfig.proxyTarget,
"/tariff.json": localConfig.proxyTarget,
"/realtime.json": localConfig.proxyTarget,
"/priceconfig.json": localConfig.proxyTarget,
"/translations.json": localConfig.proxyTarget,
"/cloudkey.json": localConfig.proxyTarget,
"/wifiscan.json": localConfig.proxyTarget,
"/save": localConfig.proxyTarget,
"/reboot": localConfig.proxyTarget,
"/configfile": localConfig.proxyTarget,
"/upgrade": localConfig.proxyTarget,
"/mqtt-ca": localConfig.proxyTarget,
"/mqtt-cert": localConfig.proxyTarget,
"/mqtt-key": localConfig.proxyTarget,
"/logo.svg": localConfig.proxyTarget,
}
}
})

View File

@@ -0,0 +1,7 @@
// Copy this file to vite.config.local.js and update with your device's IP address
// vite.config.local.js is ignored by git so your settings won't be committed
export default {
// The IP address of your AMS reader device for local development
proxyTarget: "http://192.168.4.1"
}

View File

@@ -45,8 +45,6 @@
#include "LittleFS.h"
#define WIFI_TEST_TIMEOUT 30000
class AmsWebServer {
public:
#if defined(AMS_REMOTE_DEBUG)
@@ -115,10 +113,6 @@ private:
WebServer server;
#endif
bool wifiTestInProgress = false;
unsigned long wifiTestStarted = 0;
uint8_t wifiTestStatusCode = 0;
bool checkSecurity(byte level, bool send401 = true);
void indexHtml();
@@ -143,8 +137,6 @@ private:
void cloudkeyJson();
void wifiScan();
void wifiTestStart();
void wifiTestStatus();
void configurationJson();
void handleSave();

View File

@@ -137,8 +137,6 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, AmsDa
server.on(context + F("/cloudkey.json"), HTTP_GET, std::bind(&AmsWebServer::cloudkeyJson, this));
server.on(context + F("/wifiscan.json"), HTTP_GET, std::bind(&AmsWebServer::wifiScan, this));
server.on(context + F("/wifitest.json"), HTTP_POST, std::bind(&AmsWebServer::wifiTestStart, this));
server.on(context + F("/wifitest.json"), HTTP_GET, std::bind(&AmsWebServer::wifiTestStatus, this));
server.on(context + F("/configuration.json"), HTTP_GET, std::bind(&AmsWebServer::configurationJson, this));
server.on(context + F("/save"), HTTP_POST, std::bind(&AmsWebServer::handleSave, this));
@@ -2930,47 +2928,4 @@ void AmsWebServer::wifiScan() {
server.setContentLength(strlen(buf));
server.send(200, MIME_JSON, buf);
}
void AmsWebServer::wifiTestStart() {
if(!checkSecurity(1))
return;
if(WiFi.getMode() == WIFI_AP_STA) {
wifiTestStarted = millis();
String ssid = server.arg(F("ssid"));
String psk = server.arg(F("psk"));
WiFi.begin(ssid, psk);
wifiTestInProgress = true;
wifiTestStatusCode = 0;
}
wifiTestStatus();
}
void AmsWebServer::wifiTestStatus() {
if(!checkSecurity(1))
return;
if(wifiTestInProgress) {
wifiTestStatusCode = WiFi.status();
if(wifiTestStatusCode == WL_DISCONNECTED) { // Still trying to connect
if(millis() - wifiTestStarted > WIFI_TEST_TIMEOUT) {
wifiTestInProgress = false;
wifiTestStatusCode = 99; // Custom code for timeout
}
} else {
wifiTestInProgress = false;
}
}
wifi_config_t conf;
esp_wifi_get_config((wifi_interface_t)ESP_IF_WIFI_STA, &conf);
snprintf_P(buf, BufferSize, PSTR("{\"ssid\":\"%s\",\"status\":%d,\"time\":%lu,\"ip\":\"%s\"}"), conf.sta.ssid, wifiTestStatusCode, wifiTestInProgress ? millis() - wifiTestStarted : 0, WiFi.localIP().toString().c_str());
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF);
server.setContentLength(strlen(buf));
server.send(200, MIME_JSON, buf);
}
}

View File

@@ -301,7 +301,18 @@ void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
}
case ARDUINO_EVENT_ETH_DISCONNECTED:
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: {
debugW_P(PSTR("Disconnected from network"));
if(WiFi.getMode() == WIFI_STA) {
wifi_err_reason_t reason = (wifi_err_reason_t) info.wifi_sta_disconnected.reason;
switch(reason) {
case WIFI_REASON_AUTH_FAIL:
case WIFI_REASON_NO_AP_FOUND:
if(sysConfig.dataCollectionConsent == 0) {
debugI_P(PSTR("Unable to connect to configured AP, swapping to AP mode"));
toggleSetupMode();
}
break;
}
}
break;
}
case ARDUINO_EVENT_SC_FOUND_CHANNEL:
@@ -310,9 +321,6 @@ void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
case ARDUINO_EVENT_SC_GOT_SSID_PSWD:
debugI_P(PSTR("SmartConfig got config"));
break;
default:
debugD_P(PSTR("WiFi event: %s"), WiFi.eventName(event));
break;
}
}
@@ -522,6 +530,9 @@ void setup() {
WiFi.disconnect(true);
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_OFF);
#if defined(ESP32)
WiFi.onEvent(WiFiEvent);
#endif
UpgradeInformation upinfo;
if(config.getUpgradeInformation(upinfo)) {
@@ -1207,7 +1218,7 @@ void handleSystem(unsigned long now) {
unsigned long start, end;
if(now - lastSysupdate > 60000) {
start = millis();
if(WiFi.getMode() == WIFI_STA && WiFi.status() == WL_CONNECTED) {
if(WiFi.getMode() != WIFI_AP && WiFi.status() == WL_CONNECTED) {
if(mqttHandler != NULL) {
mqttHandler->publishSystem(&hw, ps, &ea);
mqttHandler->publishFirmware();
@@ -1243,7 +1254,7 @@ void handleTemperature(unsigned long now) {
if(hw.updateTemperatures()) {
lastTemperatureRead = now;
if(mqttHandler != NULL && WiFi.getMode() == WIFI_STA && WiFi.status() == WL_CONNECTED) {
if(mqttHandler != NULL && WiFi.getMode() != WIFI_AP && WiFi.status() == WL_CONNECTED) {
mqttHandler->publishTemperatures(&config, &hw);
}
}