ctucx.git: smartie-pwa

[js] smarthome web-gui

commit c14a6fe69649cb3e34f6db407eccaec6f780d846
parent 79465aa0ec3fbc5c9ef8e5a7a29b20b512af5ef0
Author: Milan Pässler <me@pbb.lc>
Date: Mon, 3 Jun 2019 19:21:38 +0200

add sw + unified websocket
16 files changed, 402 insertions(+), 287 deletions(-)
M
.gitignore
|
3
+--
M
index.html
|
3
+--
M
rollup.config.js
|
26
++------------------------
M
src/card.js
|
2
++
M
src/index.js
|
133
+++++--------------------------------------------------------------------------
A
src/layout.js
|
131
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M
src/lights.js
|
37
++++++++-----------------------------
M
src/power-meter.js
|
105
+++++++++++++++++++++++++++-----------------------------------------------------
M
src/row.js
|
2
++
M
src/settings.js
|
2
++
M
src/spinner.js
|
2
++
A
src/state.js
|
53
+++++++++++++++++++++++++++++++++++++++++++++++++++++
A
sw.js
|
44
++++++++++++++++++++++++++++++++++++++++++++
M
websocket-relay/package.json
|
1
+
M
websocket-relay/server.js
|
140
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
M
websocket-relay/yarn.lock
|
5
+++++
diff --git a/.gitignore b/.gitignore
@@ -1,4 +1,3 @@
 node_modules
-main.es.js
+main.js
 main.min.js
-main.es.min.js
diff --git a/index.html b/index.html
@@ -15,7 +15,6 @@ body {
 	<body>
 		<smarthome-layout></smarthome-layout>
 		<noscript>JavaScript is required to use Smart Home</noscript>
-		<script nomodule src="main.min.js"></script>
-		<script type="module" src="main.es.js"></script>
+		<script type="module" src="main.js"></script>
 	</body>
 </html>
diff --git a/rollup.config.js b/rollup.config.js
@@ -9,33 +9,11 @@ export default [
 				file: pkg.module,
 				format: "es",
 				strict: true,
-				file: "main.es.js",
-				sourceMap: "inline",
-			},
-		],
-		plugins: [
-			resolve({
-				sourceMap: true
-			}),
-		],
-	},
-	{
-		input: "src/index.js",
-		output: [
-			{
-				file: pkg.module,
-				format: "es",
-				strict: true,
-				file: "main.es.min.js",
+				file: "main.js",
 			},
 		],
 		plugins: [
 			resolve(),
-			terser({
-				mangle: {
-					module: true,
-				},
-			}),
 		],
 	},
 	{

@@ -43,7 +21,7 @@ export default [
 		output: [
 			{
 				file: pkg.module,
-				format: "iife",
+				format: "es",
 				strict: true,
 				file: "main.min.js",
 			},
diff --git a/src/card.js b/src/card.js
@@ -1,3 +1,5 @@
+"use strict";
+
 import { LitElement, html, css } from "lit-element";
 
 class Card extends LitElement {
diff --git a/src/index.js b/src/index.js
@@ -1,129 +1,10 @@
-import { LitElement, html, css } from "lit-element";
+"use strict";
 
-import "@authentic/mwc-top-app-bar";
-import "@authentic/mwc-card";
-import "@authentic/mwc-icon-button";
-import "@authentic/mwc-icon";
-import "@authentic/mwc-drawer";
+import "./layout.js";
 
-import "./lights.js";
-import "./power-meter.js";
-import "./settings.js";
-import "./spinner.js";
-import "./row.js";
-
-const luciRedirect = document.createElement("script");
-luciRedirect.innerHTML = "window.location = `http://${window.location.host}/cgi-bin/luci/`;";
-
-const pages = {
-	"#/lights": { name: "Lights", content: html`<smarthome-lights></smarthome-lights>`, icon: "lightbulb" },
-	"#/powermeter": { name: "Power Meter", content: html`<smarthome-power-meter></smarthome-power-meter>`, icon: "power" },
-	"#/luci": { name: "LuCI", content: html`<smarthome-spinner></smarthome-spinner> ${luciRedirect}`, icon: "router" },
-	"#/settings": { name: "Settings", content: html`<smarthome-settings></smarthome-settings>`, icon: "settings" },
-};
-
-const notFoundPage = { name: "Sorry", content: html`<h3>The page you tried to access does not exist</h3>` };
-const defaultPage = pages["#/lights"];
-
-class SmartHomeLayout extends LitElement {
-	constructor() {
-		super(...arguments);
-		window.addEventListener("hashchange", this._handleHashChange.bind(this));
-		this._handleHashChange();
-	}
-
-	async _handleHashChange() {
-		if (!window.location.hash) {
-			this.activePage = defaultPage;
-		} else {
-			this.activePage = pages[window.location.hash] || notFoundPage;
-		}
-		await this.requestUpdate();
-		const drawer = this.shadowRoot.querySelector("mwc-drawer");
-		drawer.open = false;
-	}
-
-	_handleNavEvent(evt) {
-		const drawer = this.shadowRoot.querySelector("mwc-drawer");
-		drawer.open = !drawer.open;
-	}
-
-	render() {
-		return html`
-			<mwc-drawer type="modal">
-				<div id="drawer-content">
-					${Object.entries(pages).map(([path, page]) => html`
-						<a href="${path}" class="${this.activePage === page ? "active" : "inactive"}">
-							<mwc-icon>${page.icon}</mwc-icon>
-							<span>${page.name}</span>
-							<mwc-ripple></mwc-ripple>
-						</a>
-					`)}
-				</div>
-			</mwc-drawer>
-			<mwc-top-app-bar type="fixed" @MDCTopAppBar:nav="${this._handleNavEvent}">
-				<div id="title-container" slot="title">
-					<span>${this.activePage.name}</span>
-				</div>
-				<mwc-icon-button slot="navigationIcon" icon="menu"></mwc-icon-button>
-			</mwc-top-app-bar>
-			<div class="top-app-bar-adjust">${this.activePage.content}</div>
-		`;
-	}
-
-	static get styles() {
-		return css`
-			:host {
-				font-family: sans-serif;
-				--mdc-theme-on-primary: white;
-				--mdc-theme-primary: #43a047;
-				--mdc-theme-on-secondary: white;
-				--mdc-theme-secondary: #616161;
-				background-color: #ccc;
-			}
-			mwc-top-app-bar {
-				position: fixed;
-			}
-			.top-app-bar-adjust {
-				margin-top: 56px;
-			}
-			@media (min-width: 600px) {
-				.top-app-bar-adjust {
-					margin-top: 64px;
-				}
-				#title-container {
-					width: 100vw;
-					padding-left: calc((100% - 450px) / 2 + 20px);
-				}
-				mwc-icon-button {
-					position: absolute;
-					padding-left: calc(((100% - 450px) / 2) - 75px);
-					z-index: 10;
-				}
-			}
-
-			#drawer-content {
-				display: flex;
-				flex-direction: column;
-			}
-			#drawer-content a {
-				text-decoration: none;
-				color: black;
-				display: flex;
-				flex-direction: row;
-				margin: 0;
-				align-items: center;
-				border-bottom: 1px solid #ccc;
-			}
-			#drawer-content a mwc-icon {
-				padding: .7em;
-			}
-
-			#drawer-content a.active {
-				background-color: #ddd;
-			}
-		`;
-	}
+const sw = navigator.serviceWorker;
+if (sw) {
+	sw.register('sw.js', {
+		scope: './'
+	});
 }
-
-customElements.define("smarthome-layout", SmartHomeLayout);
diff --git a/src/layout.js b/src/layout.js
@@ -0,0 +1,131 @@
+"use strict";
+
+import { LitElement, html, css } from "lit-element";
+
+import "@authentic/mwc-top-app-bar";
+import "@authentic/mwc-card";
+import "@authentic/mwc-icon-button";
+import "@authentic/mwc-icon";
+import "@authentic/mwc-drawer";
+
+import "./lights.js";
+import "./power-meter.js";
+import "./settings.js";
+import "./spinner.js";
+import "./row.js";
+
+const luciRedirect = document.createElement("script");
+luciRedirect.innerHTML = "window.location = `http://${window.location.host}/cgi-bin/luci/`;";
+
+const pages = {
+	"#/lights": { name: "Lights", content: html`<smarthome-lights></smarthome-lights>`, icon: "lightbulb" },
+	"#/powermeter": { name: "Power Meter", content: html`<smarthome-power-meter></smarthome-power-meter>`, icon: "power" },
+	"#/luci": { name: "LuCI", content: html`<smarthome-spinner></smarthome-spinner> ${luciRedirect}`, icon: "router" },
+	"#/settings": { name: "Settings", content: html`<smarthome-settings></smarthome-settings>`, icon: "settings" },
+};
+
+const notFoundPage = { name: "Sorry", content: html`<h3>The page you tried to access does not exist</h3>` };
+const defaultPage = pages["#/lights"];
+
+class SmartHomeLayout extends LitElement {
+	constructor() {
+		super(...arguments);
+		window.addEventListener("hashchange", this._handleHashChange.bind(this));
+		this._handleHashChange();
+	}
+
+	async _handleHashChange() {
+		if (!window.location.hash) {
+			this.activePage = defaultPage;
+		} else {
+			this.activePage = pages[window.location.hash] || notFoundPage;
+		}
+		await this.requestUpdate();
+		const drawer = this.shadowRoot.querySelector("mwc-drawer");
+		drawer.open = false;
+	}
+
+	_handleNavEvent(evt) {
+		const drawer = this.shadowRoot.querySelector("mwc-drawer");
+		drawer.open = !drawer.open;
+	}
+
+	render() {
+		return html`
+			<mwc-drawer type="modal">
+				<div id="drawer-content">
+					${Object.entries(pages).map(([path, page]) => html`
+						<a href="${path}" class="${this.activePage === page ? "active" : "inactive"}">
+							<mwc-icon>${page.icon}</mwc-icon>
+							<span>${page.name}</span>
+							<mwc-ripple></mwc-ripple>
+						</a>
+					`)}
+				</div>
+			</mwc-drawer>
+			<mwc-top-app-bar type="fixed" @MDCTopAppBar:nav="${this._handleNavEvent}">
+				<div id="title-container" slot="title">
+					<span>${this.activePage.name}</span>
+				</div>
+				<mwc-icon-button slot="navigationIcon" icon="menu"></mwc-icon-button>
+			</mwc-top-app-bar>
+			<div class="top-app-bar-adjust">${this.activePage.content}</div>
+		`;
+	}
+
+	static get styles() {
+		return css`
+			:host {
+				font-family: sans-serif;
+				--mdc-theme-on-primary: white;
+				--mdc-theme-primary: #43a047;
+				--mdc-theme-on-secondary: white;
+				--mdc-theme-secondary: #616161;
+				background-color: #ccc;
+			}
+			mwc-top-app-bar {
+				position: fixed;
+			}
+			.top-app-bar-adjust {
+				margin-top: 56px;
+			}
+			@media (min-width: 600px) {
+				.top-app-bar-adjust {
+					margin-top: 64px;
+				}
+				#title-container {
+					width: 100vw;
+					padding-left: calc((100% - 450px) / 2 + 20px);
+				}
+				mwc-icon-button {
+					position: absolute;
+					padding-left: calc(((100% - 450px) / 2) - 75px);
+					z-index: 10;
+				}
+			}
+
+			#drawer-content {
+				display: flex;
+				flex-direction: column;
+			}
+			#drawer-content a {
+				text-decoration: none;
+				color: black;
+				display: flex;
+				flex-direction: row;
+				margin: 0;
+				align-items: center;
+				border-bottom: 1px solid #ccc;
+			}
+			#drawer-content a mwc-icon {
+				padding: .7em;
+			}
+
+			#drawer-content a.active {
+				background-color: #ddd;
+			}
+		`;
+	}
+}
+
+customElements.define("smarthome-layout", SmartHomeLayout);
diff --git a/src/lights.js b/src/lights.js
@@ -1,4 +1,7 @@
+"use strict";
+
 import { LitElement, html, css } from "lit-element";
+import { state } from "./state.js";
 
 import { Switch } from "@authentic/mwc-switch";
 import "@authentic/mwc-circular-progress";

@@ -10,31 +13,7 @@ import { Row } from "./row.js";
 class Lights extends LitElement {
 	constructor() {
 		super(...arguments);
-		this._initWebSocket();
-		this.connected = false;
-		this.switches = [];
-	}
-
-	_initWebSocket() {
-		this.ws = new WebSocket(`ws://${window.location.host}/relay/ws`);
-		this.ws.onopen = async () => {
-			this.connected = true;
-			this.requestUpdate();
-		};
-		this.ws.onclose = () => {
-			this.connected = false;
-			this.requestUpdate();
-			this._initWebSocket();
-		};
-		this.ws.onmessage = (msg) => {
-			clearInterval(this.timeout);
-			this.timeout = setTimeout(() => {
-				this.ws.onclose();
-			}, 4000);
-			if (!msg.data.length) return; // keepalive
-			this.switches = JSON.parse(msg.data);
-			this.requestUpdate();
-		};
+		state.subscribe(this.requestUpdate.bind(this));
 	}
 
 	_clickHandler(evt) {

@@ -42,19 +21,19 @@ class Lights extends LitElement {
 		const row = evt.composedPath().filter(el => el instanceof Row)[0];
 		const sw = row.querySelector("mwc-switch");
 		sw.checked = !sw.checked;
-		this.ws.send(`set ${Number(sw.id)} ${sw.checked ? "on" : "off"}`);
+		state.ws.send(`set ${Number(sw.id)} ${sw.checked ? "on" : "off"}`);
 		return true;
 	}
 
 	render() {
 		return html`
-			${this.connected ? html`
+			${state.data.lights ? html`
 				<smarthome-card>
-					${this.switches.map(sw => html`
+					${state.data.lights.map(sw => html`
 						<smarthome-row @click="${this._clickHandler}">
 							<span slot="left">${sw.name}</span>
 							<mwc-ripple slot="center"></mwc-ripple>
-							<mwc-switch slot="right" .id="${sw.id}" ?disabled="${!this.connected}" ?checked="${sw.value}"></mwc-switch>
+							<mwc-switch slot="right" .id="${sw.id}" ?disabled="${!state.connected}" ?checked="${sw.value}"></mwc-switch>
 						</smarthome-row>
 					`)}
 				</smarthome-card>
diff --git a/src/power-meter.js b/src/power-meter.js
@@ -1,92 +1,48 @@
+"use strict";
+
 import { LitElement, html, css } from "lit-element";
+import { state } from "./state.js";
 
 import "@authentic/mwc-circular-progress";
 import "@authentic/mwc-icon";
 import "./card.js";
 import "./spinner.js";
 
-const counters = [
-	{ id: 60, name: "Kueche" },
-	{ id: 50, name: "Sonstiges" },
-];
-
 const round = (num, places) => {
 	const factor = Math.pow(10, places);
 	return Math.round(num * factor) / factor;
 };
 
+const pad = (string, amount) => Array(amount).join('0').substr(0, amount - String(string).length) + string;
+
+const formatDate = (date) => (
+	'' +
+	date.getFullYear() +
+	'/' +
+	pad(date.getMonth() + 1, 2) +
+	'/' +
+	pad(date.getDate(), 2) +
+	' ' +
+	date.getHours() +
+	':' +
+	pad(date.getMinutes(), 2)
+);
+
 class PowerMeter extends LitElement {
 	constructor() {
 		super(...arguments);
-		this.loaded = false;
-		this.connected = false;
-		this._loadData();
-		this._initWebSocket();
-	}
-
-	_initWebSocket() {
-		this.ws = new WebSocket(`ws://${window.location.host}/gosdm/ws`);
-		this.ws.onopen = async () => {
-			this.connected = true;
-			this.requestUpdate();
-		};
-		this.ws.onclose = () => {
-			this.connected = false;
-			this.requestUpdate();
-			this._initWebSocket();
-		};
-		this.ws.onmessage = (msg) => {
-			clearInterval(this.timeout);
-			this.timeout = setTimeout(() => {
-				this.ws.onclose();
-			}, 2000);
-
-			const m = JSON.parse(msg.data);
-			if (!m.IEC61850) return;
-
-			const d = this.data[m.DeviceId];
-
-			if (m.IEC61850 === "Frequency") {
-				d.Frequency = m.Value;
-			} else if (m.IEC61850 === "VoltageL1") {
-				d.Voltage.L1 = m.Value;
-			} else if (m.IEC61850 === "CosphiL1") {
-				d.Cosphi.L1 = m.Value;
-			} else if (m.IEC61850 === "PowerL1") {
-				d.Power.L1 = m.Value;
-			} else if (m.IEC61850 === "Import") {
-				d.TotalImport = m.Value;
-			}
-
-			this.requestUpdate();
-		};
-	}
-
-	async _loadData() {
-		const newData = {};
-		await Promise.all(counters.map(async (counter) => {
-			const d = await fetch(`http://${window.location.host}/gosdm/last/${counter.id}`)
-				.then(resp => resp.json());
-
-			newData[d.ModbusDeviceId] = {
-				...d,
-				name: counter.name,
-			};
-		}));
-
-		this.data = newData;
-		this.loaded = true;
-		this.requestUpdate();
+		state.subscribe(this.requestUpdate.bind(this));
 	}
 
 	render() {
 		return html`
-			${this.loaded ? html`
+			${state.data.powermeter ? html`
 				<div id="connection-status">
-					${this.connected ? html`
-						<mwc-icon>cloud_queue</mwc-icon> Connected
+					${state.connected ? html`
+						<p><mwc-icon>cloud_queue</mwc-icon> Connected</p>
 					` : html`
-						<mwc-icon>cloud_off</mwc-icon> Disconnected
+						<p><mwc-icon>cloud_off</mwc-icon> Disconnected</p>
+						<p class="lastupdate">last updated ${formatDate(state.data.lastUpdated)}</p>
 					`}
 				</div>
 				<smarthome-card>

@@ -99,7 +55,7 @@ class PowerMeter extends LitElement {
 							<div class="table-column">Frequency</div>
 							<div class="table-column">Import</div>
 						</div>
-						${Object.entries(this.data).map(([_, d]) => html`
+						${Object.entries(state.data.powermeter).map(([_, d]) => html`
 							<div class="table-row">
 								<div class="table-column">${d.name}</div>
 								<div class="table-column">${round(d.Voltage.L1, 2)} V</div>

@@ -128,14 +84,23 @@ class PowerMeter extends LitElement {
 				--mdc-theme-secondary: #616161;
 			}
 			#connection-status mwc-icon {
-				padding: .5em;
+				padding-right: .5em;
 			}
 			#connection-status {
 				display: flex;
+				flex-direction: column;
+				justify-content: center;
+				align-items: center;
+			}
+			#connection-status>p {
+				display: flex;
 				flex-direction: row;
 				justify-content: center;
 				align-items: center;
 			}
+			#connection-status>p.lastupdate {
+				font-size: 1em;
+			}
 			.table {
 				display: flex;
 				flex-direction: row;
diff --git a/src/row.js b/src/row.js
@@ -1,3 +1,5 @@
+"use strict";
+
 import { LitElement, html, css } from "lit-element";
 
 import "@authentic/mwc-ripple";
diff --git a/src/settings.js b/src/settings.js
@@ -1,3 +1,5 @@
+"use strict";
+
 import { LitElement, html, css } from "lit-element";
 
 import "@authentic/mwc-ripple";
diff --git a/src/spinner.js b/src/spinner.js
@@ -1,3 +1,5 @@
+"use strict";
+
 import { LitElement, html, css } from "lit-element";
 
 import "@authentic/mwc-circular-progress";
diff --git a/src/state.js b/src/state.js
@@ -0,0 +1,53 @@
+"use strict";
+
+class State {
+	constructor() {
+		this.connected = false;
+		this.data = JSON.parse(localStorage.getItem("data") || "{}");
+		this.data.lastUpdated = new Date(this.data.lastUpdated);
+		this._subscribers = [];
+		this._initWS();
+	}
+
+	subscribe(callback) {
+		this._subscribers.push(callback);
+		this._updateSubscribers();
+	}
+
+	_setData(newData) {
+		this.data = newData;
+		this.data.lastUpdated = new Date();
+		localStorage.setItem("data", JSON.stringify(this.data));
+		this._updateSubscribers();
+	}
+
+	_updateSubscribers() {
+		for (let sub of this._subscribers) {
+			sub();
+		}
+	}
+
+	_initWS() {
+		this.ws = new WebSocket(`ws://192.168.1.1/relay/ws`);
+		this.ws.onclose = () => {
+			this.connected = false;
+			this._initWS();
+			this._updateSubscribers();
+		};
+		this.ws.onopen = () => {
+			this.connected = true;
+			this._updateSubscribers();
+		};
+		this.ws.onmessage = (msg) => {
+			clearInterval(this._timeout);
+			this._timeout = setTimeout(() => {
+				this.ws.close();
+				this.ws.onclose();
+			}, 2000);
+			if (!msg.data.length) return; // keepalive
+			this._setData(JSON.parse(msg.data));
+		};
+	}
+}
+
+export const state = new State();
diff --git a/sw.js b/sw.js
@@ -0,0 +1,44 @@
+'use strict';
+
+let preCache = [
+	'./',
+	'./main.js',
+	'./favicon-512x512.png',
+	'./manifest.json',
+	'https://fonts.googleapis.com/icon?family=Material+Icons',
+	'https://fonts.gstatic.com/s/materialicons/v47/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2',
+];
+
+const CACHE = 'cache-v2';
+
+self.addEventListener('install', function (evt) {
+	self.skipWaiting();
+	evt.waitUntil(caches.open(CACHE).then(function (cache) {
+		cache.addAll(preCache);
+	}));
+});
+
+self.addEventListener('fetch', function (evt) {
+	evt.respondWith(fromCache(evt.request).then(function (match) {
+		if (match) {
+			return match;
+		} else {
+			return fetch(evt.request);
+		}
+	}));
+});
+
+self.addEventListener('activate', function (event) {
+	event.waitUntil(clients.claim());
+	event.waitUntil(clients.claim().then(function () {
+		return caches.keys().then(function (cacheNames) {
+			return Promise.all(cacheNames.filter(c => c !== CACHE).map(c => caches.delete(c)));
+		});
+	}));
+});
+
+function fromCache (request) {
+	return caches.open(CACHE).then(function (cache) {
+		return cache.match(request);
+	});
+}
diff --git a/websocket-relay/package.json b/websocket-relay/package.json
@@ -1,6 +1,7 @@
 {
   "dependencies": {
     "modbus-tcp": "^0.4.13",
+    "node-fetch": "^2.6.0",
     "ws": "^7.0.0"
   }
 }
diff --git a/websocket-relay/server.js b/websocket-relay/server.js
@@ -1,29 +1,46 @@
 "use strict";
 
-const net       = require('net');
+const net       = require("net");
 const modbus    = require("modbus-tcp");
-const WebSocket = require('ws');
+const WebSocket = require("ws");
+const fetch     = require("node-fetch");
 
-const names = [
-	"Deckenbeleuchtung",
-	"Bett",
-	"Kueche",
-	"Bad",
-];
+const state = {};
+
+function broadcastState() {
+	wss.broadcast(JSON.stringify(state));
+}
 
-const ws = new WebSocket.Server({ port: 8080 });
+const wss = new WebSocket.Server({ port: 8080 });
 
-ws.broadcast = function broadcast(data) {
-	ws.clients.forEach(function each(client) {
+wss.broadcast = function broadcast(data) {
+	wss.clients.forEach(function each(client) {
 		if (client.readyState === WebSocket.OPEN) {
 			client.send(data);
 		}
 	});
 };
 
-ws.on('connection', function connection(ws) {
+wss.on('connection', function connection(ws) {
+	ws.send(JSON.stringify(state));
+});
+
+setInterval(function sendKeepalive() {
+	wss.broadcast("");
+}, 2000);
+
+/* lights */
+
+const lights = [
+	"Deckenbeleuchtung",
+	"Bett",
+	"Kueche",
+	"Bad",
+];
+
+wss.on('connection', function connection(ws) {
 	ws.on('message', function incoming(message) {
-		let input = message.split(' ');
+		const input = String(message).split(' ');
 
 		if (input[0] == 'set' && input[1] < 9 && (input[2] == 'on' || input[2] == 'off')) {
 			let val = 0;

@@ -31,20 +48,14 @@ ws.on('connection', function connection(ws) {
 			if (input[2] == 'on') {
 				val = 1;
 			}
-			
+
 			setRelay(input[1], val);
 		}
- 	});
-
-	readRealys(ws);
+	});
 });
 
-setInterval(function sendKeepalive() {
-	ws.broadcast("");
-}, 2000);
-
 function mapToNames(data) {
-	return names.map((name, id) => {
+	return lights.map((name, id) => {
 		return {
 			name,
 			id: id + 1,

@@ -53,34 +64,95 @@ function mapToNames(data) {
 	});
 }
 
-function readRealys(wss) {
-	let client = new modbus.Client();
-	let socket = new net.Socket();
+function setRelay(id, val) {
+	const client = new modbus.Client();
+	const socket = new net.Socket();
 
 
 	socket.connect({'host': '192.168.1.1', 'port': 502 });
 	client.pipe(socket);
 
+	client.writeSingleCoil(10, '10'+id, val);
+
 	client.readCoils(10, 101, 108, function (err, data){
-		wss.send(JSON.stringify(mapToNames(data)));
-	});
+		state.lights = mapToNames(data);
+		broadcastState();
+	})
 
 	socket.end();
 }
 
-function setRelay(id, val) {
-	let client = new modbus.Client();
-	let socket = new net.Socket();
+function readRelays(ws) {
+	const client = new modbus.Client();
+	const socket = new net.Socket();
 
 
 	socket.connect({'host': '192.168.1.1', 'port': 502 });
 	client.pipe(socket);
 
-	client.writeSingleCoil(10, '10'+id, val);
-
 	client.readCoils(10, 101, 108, function (err, data){
-		ws.broadcast(JSON.stringify(mapToNames(data)));
-	})
+		state.lights = mapToNames(data);
+	});
 
 	socket.end();
 }
+
+readRelays();
+
+/* powermeter */
+
+const counters = [
+	{ id: 60, name: "Kueche" },
+	{ id: 50, name: "Sonstiges" },
+];
+
+let timeout;
+let gosdm;
+
+async function initWS() {
+	state.powermeter = {};
+	await Promise.all(counters.map(async function(counter) {
+		const d = await fetch(`http://127.0.0.1/gosdm/last/${counter.id}`)
+			.then(resp => resp.json());
+
+		state.powermeter[d.ModbusDeviceId] = {
+			...d,
+			name: counter.name,
+		};
+	}));
+	broadcastState();
+
+	gosdm = new WebSocket(`ws://127.0.0.1/gosdm/ws`);
+	gosdm.onclose = () => {
+		state.powermeter = undefined;
+		broadcastState();
+		initWS();
+	};
+	gosdm.onmessage = (msg) => {
+		clearTimeout(timeout);
+		timeout = setTimeout(() => {
+			gosdm.close();
+		}, 5000);
+
+		const m = JSON.parse(msg.data);
+		if (!m.IEC61850) return;
+
+		const d = state.powermeter[m.DeviceId];
+
+		if (m.IEC61850 === "Frequency") {
+			d.Frequency = m.Value;
+		} else if (m.IEC61850 === "VoltageL1") {
+			d.Voltage.L1 = m.Value;
+		} else if (m.IEC61850 === "CosphiL1") {
+			d.Cosphi.L1 = m.Value;
+		} else if (m.IEC61850 === "PowerL1") {
+			d.Power.L1 = m.Value;
+		} else if (m.IEC61850 === "Import") {
+			d.TotalImport = m.Value;
+		}
+
+		broadcastState();
+	};
+}
+
+initWS();
diff --git a/websocket-relay/yarn.lock b/websocket-relay/yarn.lock
@@ -12,6 +12,11 @@ modbus-tcp@^0.4.13:
   resolved "https://registry.yarnpkg.com/modbus-tcp/-/modbus-tcp-0.4.13.tgz#3e0e93af516d78a19f0f62686e64558e6ae6426b"
   integrity sha512-TytYObBTQhEMzjfFgGK3iESUoneH9WJRWHoZkt7PBuNnnx2nSOW0tOLC1YeQKNi/Qv7Xv0IjbVhxOoHyXOaV9g==
 
+node-fetch@^2.6.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
+  integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
+
 ws@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.0.0.tgz#79351cbc3f784b3c20d0821baf4b4ff809ffbf51"