Created new UI with Svelte

This commit is contained in:
Gunnar Skjold 2022-09-03 12:19:56 +02:00
parent e232b875fa
commit 8b0d4185d3
33 changed files with 4283 additions and 0 deletions

2
.gitignore vendored
View File

@ -15,3 +15,5 @@ platformio-user.ini
/sdkconfig
/.tmp
/*.zip
node_modules
/gui/dist

24
ui/svelte/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

12
ui/svelte/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AMS reader</title>
</head>
<body class="bg-gray-100">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

34
ui/svelte/jsconfig.json Normal file
View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"moduleResolution": "Node",
"target": "ESNext",
"module": "ESNext",
/**
* svelte-preprocess cannot figure out whether you have
* a value or a type, so tell TypeScript to enforce using
* `import type` instead of `import` for Types.
*/
"importsNotUsedAsValues": "error",
"isolatedModules": true,
"resolveJsonModule": true,
/**
* To have warnings / errors of the Svelte compiler at the
* correct position, enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable this if you'd like to use dynamic types.
*/
"checkJs": true
},
/**
* Use global.d.ts instead of compilerOptions.types
* to avoid limiting type declarations.
*/
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
}

3106
ui/svelte/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
ui/svelte/package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "svelte-gui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"svelte": "^3.49.0",
"vite": "^3.0.7",
"postcss": "^8.4.14",
"postcss-load-config": "^4.0.1",
"svelte-preprocess": "^4.10.7",
"autoprefixer": "^10.4.7",
"tailwindcss": "^3.1.5",
"http-proxy-middleware": "^2.0.1"
}
}

View File

@ -0,0 +1,13 @@
const tailwindcss = require("tailwindcss");
const autoprefixer = require("autoprefixer");
const config = {
plugins: [
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(),
//But others, like autoprefixer, need to run after,
autoprefixer,
],
};
module.exports = config;

15
ui/svelte/src/App.svelte Normal file
View File

@ -0,0 +1,15 @@
<script>
import { dataStore } from './lib/DataStores.js';
import Header from './lib/Header.svelte';
import Dashboard from './lib/Dashboard.svelte';
let data = {};
dataStore.subscribe(update => {
data = update;
});
</script>
<div class="container mx-auto m-3">
<Header data={data}/>
<Dashboard data={data}/>
</div>

View File

@ -0,0 +1,8 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.gh-logo {
width: 2rem;
height: 2rem;
}

View File

@ -0,0 +1,6 @@
<?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 512 499.36" focusable="false">
<title>GitHub</title>
<path d="M256 0C114.64 0 0 114.61 0 256c0 113.09 73.34 209 175.08 242.9 12.8 2.35 17.47-5.56 17.47-12.34 0-6.08-.22-22.18-.35-43.54-71.2 15.49-86.2-34.34-86.2-34.34-11.64-29.57-28.42-37.45-28.42-37.45-23.27-15.84 1.73-15.55 1.73-15.55 25.69 1.81 39.21 26.38 39.21 26.38 22.84 39.12 59.92 27.82 74.5 21.27 2.33-16.54 8.94-27.82 16.25-34.22-56.84-6.43-116.6-28.43-116.6-126.49 0-27.95 10-50.8 26.35-68.69-2.63-6.48-11.42-32.5 2.51-67.75 0 0 21.49-6.88 70.4 26.24a242.65 242.65 0 0 1 128.18 0c48.87-33.13 70.33-26.24 70.33-26.24 14 35.25 5.18 61.27 2.55 67.75 16.41 17.9 26.31 40.75 26.31 68.69 0 98.35-59.85 120-116.88 126.32 9.19 7.9 17.38 23.53 17.38 47.41 0 34.22-.31 61.83-.31 70.23 0 6.85 4.61 14.81 17.6 12.31C438.72 464.97 512 369.08 512 256.02 512 114.62 397.37 0 256 0z" fill="#f8f9fa" fill-rule="evenodd"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,33 @@
<script>
export let data;
export let currency;
</script>
<div class="mx-2 text-sm">
<strong>Real time calculation</strong>
<div class="grid grid-cols-2 mt-4">
<div>Hour</div>
<div class="text-right">{data && data.h && data.h.u ? data.h.u.toFixed(2) : '-'} kWh</div>
<div>Day</div>
<div class="text-right">{data && data.d && data.d.u ? data.d.u.toFixed(1) : '-'} kWh</div>
<div>Month</div>
<div class="text-right">{data && data.m && data.m.u ? data.m.u.toFixed(0) : '-'} kWh</div>
</div>
{#if currency}
<div class="grid grid-cols-2 mt-4">
<div>Hour</div>
<div class="text-right">{data && data.h && data.h.c ? data.h.c.toFixed(2) : '-'} {currency}</div>
<div>Day</div>
<div class="text-right">{data && data.d && data.d.c ? data.d.c.toFixed(1) : '-'} {currency}</div>
<div>Month</div>
<div class="text-right">{data && data.m && data.m.c ? data.m.c.toFixed(0) : '-'} {currency}</div>
</div>
{/if}
<div class="grid grid-cols-2 mt-4">
<div>Max</div>
<div class="text-right">
{data && data.x ? data.x.toFixed(1) : '-'} / {data && data.t ? data.t.toFixed(1) : '-'} kWh
</div>
</div>
</div>

View File

@ -0,0 +1,64 @@
<script>
import BarChart from './BarChart.svelte';
import { ampcol } from './Helpers.js';
export let u1;
export let u2;
export let u3;
export let i1;
export let i2;
export let i3;
export let max;
let config = {};
$: {
let xTicks = [];
let points = [];
if(u1 > 0) {
xTicks.push({ label: 'L1' });
points.push({
label: i1 ? i1 + 'A' : '-',
value: i1 ? i1 : 0,
color: ampcol(i1 ? (i1)/(max)*100 : 0)
});
}
if(u2 > 0) {
xTicks.push({ label: 'L2' });
points.push({
label: i2 ? i2 + 'A' : '-',
value: i2 ? i2 : 0,
color: ampcol(i2 ? (i2)/(max)*100 : 0)
});
}
if(u3 > 0) {
xTicks.push({ label: 'L3' });
points.push({
label: i3 ? i3 + 'A' : '-',
value: i3 ? i3 : 0,
color: ampcol(i3 ? (i3)/(max)*100 : 0)
});
}
config = {
height: 250,
width: 224,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: 0,
max: max,
ticks: [
{ value: 0, label: '0%' },
{ value: max/4, label: '25%' },
{ value: max/2, label: '50%' },
{ value: (max/4)*3, label: '75%' },
{ value: max, label: '100%' }
]
},
x: {
ticks: xTicks
},
points: points
};
};
</script>
<BarChart config={config} />

View File

@ -0,0 +1,14 @@
<script>
export let color;
export let title;
export let text;
</script>
{#if color == 'green'}
<span title={title} class="my-auto bg-green-500 text-green-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">{text}</span>
{:else if color === `yellow`}
<span title={title} class="my-auto bg-yellow-500 text-yellow-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">{text}</span>
{:else if color === `red`}
<span title={title} class="my-auto bg-red-500 text-red-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">{text}</span>
{:else if color === `gray`}
<span title={title} class="my-auto bg-gray-500 text-gray-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">{text}</span>
{/if}

View File

@ -0,0 +1,121 @@
<script>
export let config;
let barWidth;
let xScale;
let yScale;
$: {
let innerWidth = config.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);
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;
};
};
</script>
<div class="chart" bind:clientWidth={config.width} bind:clientHeight={config.height}>
<svg height="{config.height}">
<!-- y axis -->
<g class="axis y-axis">
{#each config.y.ticks as tick}
<g class="tick tick-{tick.value}" transform="translate(0, {yScale(tick.value)})">
<line x2="100%"></line>
<text y="-4">{tick.label}</text>
</g>
{/each}
</g>
<!-- x axis -->
<g class="axis x-axis">
{#each config.x.ticks as point, i}
<g class="tick" transform="translate({xScale(i)},{config.height})">
<text x="{barWidth/2}" y="-4">{point.label}</text>
</g>
{/each}
</g>
<g class='bars'>
{#each config.points as point, i}
<rect
x="{xScale(i) + 2}"
y="{yScale(point.value)}"
width="{barWidth - 4}"
height="{yScale(0) - yScale(point.value)}"
fill="{point.color}"
/>
<text
y="{yScale(point.value) > yScale(0)-15 ? yScale(point.value) - 12 : yScale(point.value)+10}"
x="{xScale(i) + barWidth/2}"
width="{barWidth - 4}"
dominant-baseline="middle"
text-anchor="{barWidth < 25 ? 'left' : 'middle'}"
fill="{yScale(point.value) > yScale(0)-15 ? point.color : 'white'}"
transform="rotate({barWidth < 25 ? 90 : 0}, {xScale(i) + (barWidth/2)}, {yScale(point.value) > yScale(0)-12 ? yScale(point.value) - 12 : yScale(point.value)+10})"
>{point.label}</text>
{/each}
</g>
</svg>
</div>
<style>
h2 {
text-align: center;
}
.chart {
width: 100%;
margin: 0 auto;
}
svg {
position: relative;
width: 100%;
}
.tick {
font-family: Helvetica, Arial;
font-size: .725em;
font-weight: 200;
}
.tick line {
stroke: #e2e2e2;
stroke-dasharray: 2;
}
.tick text {
fill: #999;
text-anchor: start;
}
.tick.tick-0 line {
stroke-dasharray: 0;
}
.x-axis .tick text {
text-anchor: middle;
}
.bars rect {
stroke: rgb(0,0,0);
stroke-opacity: 0.25;
opacity: 0.9;
}
.bars text {
font-family: Helvetica, Arial;
font-size: .725em;
display: block;
text-align: center;
}
</style>

View File

@ -0,0 +1,12 @@
<script>
import { zeropad } from './Helpers.js';
export let timestamp;
let monthnames = ["","Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
</script>
{#if Math.abs(new Date().getTime()-timestamp.getTime()) < 300000 }
{`${zeropad(timestamp.getDate())}. ${monthnames[timestamp.getMonth()]} ${zeropad(timestamp.getHours())}:${zeropad(timestamp.getMinutes())}`}
{:else}
<span class="text-red-500">{`${zeropad(timestamp.getDate())}.${zeropad(timestamp.getMonth())}.${timestamp.getFullYear()} ${zeropad(timestamp.getHours())}:${zeropad(timestamp.getMinutes())}`}</span>
{/if}

View File

@ -0,0 +1,10 @@
<script>
let count = 0
const increment = () => {
count += 1
}
</script>
<button on:click={increment}>
count is {count}
</button>

View File

@ -0,0 +1,78 @@
<script>
import { pricesStore, dayPlotStore, monthPlotStore, temperaturesStore } from './DataStores.js';
import { metertype } from './Helpers.js';
import PowerGauge from './PowerGauge.svelte';
import VoltPlot from './VoltPlot.svelte';
import AmpPlot from './AmpPlot.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';
export let data = {}
let prices = {}
let dayPlot = {}
let monthPlot = {}
let temperatures = {};
pricesStore.subscribe(update => {
prices = update;
});
dayPlotStore.subscribe(update => {
dayPlot = update;
});
monthPlotStore.subscribe(update => {
monthPlot = update;
});
temperaturesStore.subscribe(update => {
temperatures = update;
});
</script>
<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>{data.mt ? metertype(data.mt) : '-'}</div>
<div class="text-right">{data.ic ? data.ic.toFixed(1) : '-'} kWh</div>
</div>
</div>
{#if data.om}
<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></div>
<div class="text-right">{data.ec ? data.ec.toFixed(1) : '-'} kWh</div>
</div>
</div>
{/if}
<div class="bg-white m-2 p-2 rounded shadow-lg">
<VoltPlot u1={data.u1} u2={data.u2} u3={data.u3} ds={data.ds}/>
</div>
<div class="bg-white m-2 p-2 rounded shadow-lg">
<AmpPlot u1={data.u1} u2={data.u2} u3={data.u3} i1={data.i1} i2={data.i2} i3={data.i3} max={data.mf ? data.mf : 32}/>
</div>
<div class="bg-white m-2 p-2 rounded shadow-lg">
<ReactiveData importInstant={data.ri} exportInstant={data.re} importTotal={data.ric} exportTotal={data.rec}/>
</div>
<div class="bg-white m-2 p-2 rounded shadow-lg">
<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">
<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">
<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">
<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">
<TemperaturePlot json={temperatures} />
</div>
{/if}
</div>

View File

@ -0,0 +1,72 @@
import { readable } from 'svelte/store';
let data = {};
export const dataStore = readable(data, (set) => {
async function getData(){
const response = await fetch("/data.json");
data = (await response.json())
set(data);
}
const interval = setInterval(getData, 5000);
getData();
return function stop() {
clearInterval(interval);
}
});
let prices = {};
export const pricesStore = readable(prices, (set) => {
async function getPrices(){
const response = await fetch("/energyprice.json");
prices = (await response.json())
set(prices);
}
const date = new Date();
const timeout = setTimeout(getPrices, (61-date.getMinutes())*60000)
getPrices();
return function stop() {}
});
let dayPlot = {};
export const dayPlotStore = readable(dayPlot, (set) => {
async function getDayPlot(){
const response = await fetch("/dayplot.json");
dayPlot = (await response.json())
set(dayPlot);
}
const date = new Date();
const timeout = setTimeout(getDayPlot, (61-date.getMinutes())*60000)
getDayPlot();
return function stop() {
clearTimeout(timeout);
}
});
let monthPlot = {};
export const monthPlotStore = readable(monthPlot, (set) => {
async function getmonthPlot(){
const response = await fetch("/monthplot.json");
monthPlot = (await response.json())
set(monthPlot);
}
const date = new Date();
const timeout = setTimeout(getmonthPlot, (24-date.getHours())*3600000)
getmonthPlot();
return function stop() {
clearTimeout(timeout);
}
});
let temperatures = {};
export const temperaturesStore = readable(temperatures, (set) => {
async function getTemperatures(){
const response = await fetch("/temperature.json");
temperatures = (await response.json())
set(temperatures);
}
const interval = setInterval(getTemperatures, 60000);
getTemperatures();
return function stop() {
clearTimeout(interval);
}
});

View File

@ -0,0 +1,92 @@
<script>
import { zeropad } from './Helpers.js';
import BarChart from './BarChart.svelte';
export let json;
let config = {};
let max = 0;
let min = 0;
$: {
let hour = new Date().getUTCHours();
let i = 0;
let val = 0;
let imp = 0;
let exp = 0;
let h = 0;
let yTicks = [];
let xTicks = [];
let points = [];
let cur = new Date();
for(i = hour; i<24; i++) {
imp = json["i"+zeropad(i)];
exp = json["e"+zeropad(i)];
val = imp-exp;
if(!val) val = 0;
cur.setUTCHours(i);
xTicks.push({
label: zeropad(cur.getHours())
});
points.push({
label: val.toFixed(2),
value: val,
color: '#7c3aed'
});
min = Math.min(min, val);
max = Math.max(max, val);
};
for(i = 0; i < hour; i++) {
imp = json["i"+zeropad(i)];
exp = json["e"+zeropad(i)];
val = imp-exp;
if(!val) val = 0;
cur.setUTCHours(i);
xTicks.push({
label: zeropad(cur.getHours())
});
points.push({
label: val.toFixed(2),
value: val,
color: '#7c3aed'
});
min = Math.min(min, val);
max = Math.max(max, val);
};
max = Math.ceil(max);
min = Math.floor(min);
let range = max;
if(min < 0) range += Math.abs(min);
let yTickDist = range/4;
for(i = 0; i < 5; i++) {
val = min + (yTickDist*i);
yTicks.push({
value: val,
label: val.toFixed(1)
});
}
config = {
height: 226,
width: 1520,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: yTicks
},
x: {
ticks: xTicks
},
points: points
};
};
</script>
<div class="mx-2">
<strong class="text-sm">Energy use last 24 hours (kWh)</strong>
<BarChart config={config} />
</div>

View File

@ -0,0 +1,35 @@
<script>
import GitHubLogo from './../assets/github.svg';
import Badge from './Badge.svelte';
import Clock from './Clock.svelte';
export let data = {}
let timestamp = new Date(0);
</script>
<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">
<a href="/">AMS reader <span>v0.0.0</span></a>
</div>
<div class="flex-none my-auto p-2 flex space-x-4">
<div class="flex-none my-auto">Up { data.u ? data.u : '-' }</div>
<div class="flex-none my-auto">{ data.t ? data.t.toFixed(1) : '-' }&deg;C</div>
<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={data.v > 0 ? data.v.toFixed(2)+"V" : "ESP"} color={data.em === 1 ? 'green' : data.em === 2 ? 'yellow' : data.em === 3 ? 'red' : 'gray'}/>
<Badge title="HAN" text="HAN" color={data.hm === 1 ? 'green' : data.hm === 2 ? 'yellow' : data.em === 3 ? 'red' : 'gray'}/>
<Badge title="WiFi" text={data.r ? data.r.toFixed(0)+"dBm" : "WiFi"} color={data.wm === 1 ? 'green' : data.wm === 2 ? 'yellow' : data.em === 3 ? 'red' : 'gray'}/>
<Badge title="MQTT" text="MQTT" color={data.mm === 1 ? 'green' : data.mm === 2 ? 'yellow' : data.em === 3 ? 'red' : 'gray'}/>
</div>
<div class="flex-auto p-2 flex flex-row-reverse">
<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>
<div class="flex-none my-auto px-2">
<Clock timestamp={ data.c ? new Date(data.c * 1000) : new Date(0) } />
</div>
</div>
</div>
</nav>

View File

@ -0,0 +1,44 @@
export function voltcol(pct) {
if(pct > 85) return '#d90000';
else if(pct > 75) return'#e32100';
else if(pct > 70) return '#ffb800';
else if(pct > 65) return '#dcd800';
else if(pct > 35) return '#32d900';
else if(pct > 25) return '#dcd800';
else if(pct > 20) return '#ffb800';
else if(pct > 15) return'#e32100';
else return '#d90000';
};
export function ampcol(pct) {
if(pct > 90) return '#d90000';
else if(pct > 85) return'#e32100';
else if(pct > 80) return '#ffb800';
else if(pct > 75) return '#dcd800';
else return '#32d900';
};
export function metertype(mt) {
switch(mt) {
case 1:
return "Aidon";
case 2:
return "Kaifa";
case 3:
return "Kamstrup";
case 8:
return "Iskra";
case 9:
return "Landis";
case 10:
return "Sagemcom";
default:
return "";
}
}
export function zeropad(num) {
num = num.toString();
while (num.length < 2) num = "0" + num;
return num;
}

View File

@ -0,0 +1,93 @@
<script>
import { zeropad } from './Helpers.js';
import BarChart from './BarChart.svelte';
export let json;
let config = {};
let max = 0;
let min = 0;
$: {
let hour = new Date().getUTCHours();
let i = 0;
let val = 0;
let imp = 0;
let exp = 0;
let h = 0;
let yTicks = [];
let xTicks = [];
let points = [];
let cur = new Date();
let lm = new Date();
lm.setDate(0);
for(i = cur.getDate(); i<=lm.getDate(); i++) {
imp = json["i"+zeropad(i)];
exp = json["e"+zeropad(i)];
val = imp-exp;
if(!val) val = 0;
xTicks.push({
label: zeropad(i)
});
points.push({
label: val.toFixed(0),
value: val,
color: '#7c3aed'
});
min = Math.min(min, val);
max = Math.max(max, val);
}
for(i = 1; i < cur.getDate(); i++) {
imp = json["i"+zeropad(i)];
exp = json["e"+zeropad(i)];
val = imp-exp;
if(!val) val = 0;
xTicks.push({
label: zeropad(i)
});
points.push({
label: val.toFixed(0),
value: val,
color: '#7c3aed'
});
min = Math.min(min, val);
max = Math.max(max, val);
}
max = Math.ceil(max/10)*10;
min = Math.floor(min/10)*10;
let range = max;
if(min < 0) range += Math.abs(min);
let yTickDist = range/4;
for(i = 0; i < 5; i++) {
val = min + (yTickDist*i);
yTicks.push({
value: val,
label: val.toFixed(0)
});
}
config = {
height: 226,
width: 1520,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: yTicks
},
x: {
ticks: xTicks
},
points: points
};
};
</script>
<div class="mx-2">
<strong class="text-sm">Energy use last month (kWh)</strong>
<BarChart config={config} />
</div>

View File

@ -0,0 +1,43 @@
<script>
import PowerGaugeSvg from './PowerGaugeSvg.svelte';
import { ampcol } from './Helpers.js';
export let val;
export let max;
export let unit;
export let label;
</script>
<div class="overlay-plot">
<PowerGaugeSvg pct={val/max * 100} color={ampcol(val/max * 100)}/>
<span class="plot-overlay">
<span class="plot-value">{val}</span>
<span class="plot-unit">{unit}</span>
<br/>
<span class="plot-label">{label}</span>
</span>
</div>
<style>
.overlay-plot {
position: relative;
}
.plot-overlay {
position: absolute;
top: 35%;
left: 25%;
width: 50%;
text-align: center;
}
.plot-value {
font-size: 1.7rem;
cursor: pointer;
}
.plot-unit {
font-size: 1.0rem;
color: grey;
}
.plot-label {
font-size: 1.0rem;
}
</style>

View File

@ -0,0 +1,39 @@
<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;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
function describeArc(x, y, radius, startAngle, endAngle){
var start = polarToCartesian(x, y, radius, endAngle);
var end = polarToCartesian(x, y, radius, startAngle);
var arcSweep = endAngle - startAngle <= 180 ? "0" : "1";
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, arcSweep, 0, end.x, end.y
].join(" ");
return d;
}
</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">
<path d="{ describeArc(150, 150, 115, 210, 510) }" stroke="#eee" fill="none" stroke-width="55"/>
<path d="{ describeArc(150, 150, 115, 210, 210 + (510*pct/100)) }" stroke={color} fill="none" stroke-width="55"/>
</svg>
</div>

View File

@ -0,0 +1,85 @@
<script>
import { zeropad } from './Helpers.js';
import BarChart from './BarChart.svelte';
export let json;
let config = {};
let max = 0;
let min = 0;
$: {
let hour = new Date().getUTCHours();
let i = 0;
let val = 0;
let h = 0;
let d = json["20"] == null ? 2 : 1;
let yTicks = [];
let xTicks = [];
let points = [];
let cur = new Date();
for(i = hour; i<24; i++) {
cur.setUTCHours(i);
val = json[zeropad(h++)];
if(val == null) break;
xTicks.push({
label: zeropad(cur.getHours())
});
points.push({
label: val.toFixed(d),
value: val,
color: '#7c3aed'
});
min = Math.min(min, val);
max = Math.max(max, val);
};
for(i = 0; i < 24; i++) {
cur.setUTCHours(i);
val = json[zeropad(h++)];
if(val == null) break;
xTicks.push({
label: zeropad(cur.getHours())
});
points.push({
label: val.toFixed(d),
value: val,
color: '#7c3aed'
});
min = Math.min(min, val);
max = Math.max(max, val);
};
max = Math.ceil(max);
min = Math.floor(min);
let range = max;
if(min < 0) range += Math.abs(min);
let yTickDist = range/4;
for(i = 0; i < 5; i++) {
val = min + (yTickDist*i);
yTicks.push({
value: val,
label: val.toFixed(2)
});
}
config = {
height: 226,
width: 1520,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: yTicks
},
x: {
ticks: xTicks
},
points: points
};
};
</script>
<div class="mx-2">
<strong class="text-sm">Future energy price ({json.currency})</strong>
<BarChart config={config} />
</div>

View File

@ -0,0 +1,24 @@
<script>
export let importInstant;
export let exportInstant;
export let importTotal;
export let exportTotal;
</script>
<div class="mx-2">
<strong class="text-sm">Reactive</strong>
<div class="grid grid-cols-2 mt-4">
<div>Instant in</div>
<div class="text-right">{typeof importInstant !== 'undefined' ? importInstant.toFixed(0) : '-'} VAr</div>
<div>Instant out</div>
<div class="text-right">{typeof exportInstant !== 'undefined' ? exportInstant.toFixed(0) : '-'} VAr</div>
</div>
<div class="grid grid-cols-2 mt-4">
<div>Total in</div>
<div class="text-right">{typeof importTotal !== 'undefined' ? importTotal.toFixed(1) : '-'} kVArh</div>
<div>Total out</div>
<div class="text-right">{typeof exportTotal !== 'undefined' ? exportTotal.toFixed(1) : '-'} kVArh</div>
</div>
</div>

View File

@ -0,0 +1,72 @@
<script>
import { zeropad } from './Helpers.js';
import BarChart from './BarChart.svelte';
export let json;
let config = {};
let max = 0;
let min = 0;
$: {
let hour = new Date().getUTCHours();
let i = 0;
let val = 0;
let imp = 0;
let exp = 0;
let h = 0;
let yTicks = [];
let xTicks = [];
let points = [];
if(json.s) {
json.s.forEach((obj, i) => {
var name = obj.n ? obj.n : obj.a;
val = obj.v;
if(val == -127) val = 0;
xTicks.push({
label: name.slice(-4)
});
points.push({
label: val.toFixed(1),
value: val,
color: '#7c3aed'
});
min = Math.min(min, val);
max = Math.max(max, val);
});
}
max = Math.ceil(max);
min = Math.floor(min);
let range = max;
if(min < 0) range += Math.abs(min);
let yTickDist = range/4;
for(i = 0; i < 5; i++) {
val = min + (yTickDist*i);
yTicks.push({
value: val,
label: val.toFixed(1)
});
}
config = {
height: 226,
width: 1520,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: yTicks
},
x: {
ticks: xTicks
},
points: points
};
};
</script>
<div class="mx-2">
<strong class="text-sm">Temperature sensors (&deg;C)</strong>
<BarChart config={config} />
</div>

View File

@ -0,0 +1,61 @@
<script>
import BarChart from './BarChart.svelte';
import { voltcol } from './Helpers.js';
export let u1;
export let u2;
export let u3;
export let ds;
let min = 200;
let max = 260;
let config = {};
$: {
let xTicks = [];
let points = [];
if(u1 > 0) {
xTicks.push({ label: ds === 1 ? 'L1-L2' : 'L1' });
points.push({
label: u1 ? u1 + 'V' : '-',
value: u1 ? u1 : 0,
color: voltcol(u1 ? (u1-min)/(max-min)*100 : 0)
});
}
if(u2 > 0) {
xTicks.push({ label: ds === 1 ? 'L1-L3' : 'L2' });
points.push({
label: u2 ? u2 + 'V' : '-',
value: u2 ? u2 : 0,
color: voltcol(u2 ? (u2-min)/(max-min)*100 : 0)
});
}
if(u3 > 0) {
xTicks.push({ label: ds === 1 ? 'L2-L3' : 'L3' });
points.push({
label: u3 ? u3 + 'V' : '-',
value: u3 ? u3 : 0,
color: voltcol(u3 ? (u3-min)/(max-min)*100 : 0)
});
}
config = {
height: 250,
width: 224,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: [
{ value: 207, label: '-10%' },
{ value: 230, label: '230v' },
{ value: 253, label: '+10%' }
]
},
x: {
ticks: xTicks
},
points: points
};
}
</script>
<BarChart config={config} />

9
ui/svelte/src/main.js Normal file
View File

@ -0,0 +1,9 @@
import "./app.postcss";
import App from "./App.svelte";
const app = new App({
target: document.getElementById("app"),
});
export default app;

2
ui/svelte/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@ -0,0 +1,11 @@
import preprocess from "svelte-preprocess";
const config = {
preprocess: [
preprocess({
postcss: true,
}),
],
};
export default config;

View File

@ -0,0 +1,11 @@
const config = {
content: ["./index.html","./src/**/*.{html,js,svelte,ts}"],
theme: {
extend: {},
},
plugins: [],
};
module.exports = config;

16
ui/svelte/vite.config.js Normal file
View File

@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte()],
server: {
proxy: {
"/data.json": "http://192.168.233.235",
"/energyprice.json": "http://192.168.233.235",
"/dayplot.json": "http://192.168.233.235",
"/monthplot.json": "http://192.168.233.235",
"/temperature.json": "http://192.168.233.235",
}
}
})