ctucx.git: smartie-pwa

[js] smarthome web-gui

commit fa14ce28daa297884371c213ae10e67e31d9a736
parent 3d0277f80c9b4d74d36621b6de79c65f2d1065bc
Author: Milan Pässler <me@pbb.lc>
Date: Sun, 21 Jul 2019 22:14:01 +0200

add temperature page
6 files changed, 422 insertions(+), 9 deletions(-)
M
src/layout.js
|
16
+++++++++-------
M
src/power-meter/live.js
|
2
+-
M
src/state.js
|
11
++++++++++-
A
src/temperature/history.js
|
196
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/temperature/index.js
|
74
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/temperature/live.js
|
132
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/src/layout.js b/src/layout.js
@@ -11,20 +11,23 @@ import "@authentic/mwc-drawer";
 
 import "./switches.js";
 import "./power-meter/index.js";
+import "./temperature/index.js";
 import "./departures.js";
 import "./settings.js";
 import "./spinner.js";
 import "./row.js";
 
 const createRedirect = (id) => {
-	const config = state.config.views.filter(view => view.url == id)[0];
+	const config = state.config.views.filter(view => view.url == id)[0] || state.config.views[0];
 	const redirect = document.createElement("script");
-	redirect.innerHTML = `window.location = "${config.destination}";`;
+	redirect.innerHTML = `window.location.href = "${config.destination}"; console.log(window.location.href)`;
 };
 
 const viewTypes = {
+	"noconfig": id => html`<smarthome-spinner>Waiting for initial connection...</smarthome-spinner>`,
 	"switches": id => html`<smarthome-switches .viewid="${id}"></smarthome-switches>`,
 	"powermeter": id => html`<smarthome-power-meter .viewid="${id}"></smarthome-power-meter>`, icon: "power" ,
+	"temperature": id => html`<smarthome-temperature .viewid="${id}"></smarthome-temperature>`, icon: "thermometer" ,
 	"departures": id => html`<smarthome-departures .viewid="${id}"></smarthome-departures>`,
 	"redirect": id => html`<smarthome-spinner .viewid="${id}">Redirecting...</smarthome-spinner> ${createRedirect(id)}`,
 	"settings": id => html`<smarthome-settings></smarthome-settings>`,

@@ -41,11 +44,7 @@ class SmartHomeLayout extends LitElement {
 	}
 
 	async _handleHashChange() {
-		if (!window.location.hash) {
-			this.activeView = state.config.views[0];
-		} else {
-			this.activeView = state.config.views.filter(view => `#/${view.url}` == window.location.hash)[0] || notFoundView;
-		}
+		this.activeView = state.config.views.filter(view => `#/${view.url}` == window.location.hash)[0] || state.config.views[0];
 		await this.requestUpdate();
 		const drawer = this.shadowRoot.querySelector("mwc-drawer");
 		drawer.open = false;

@@ -58,6 +57,9 @@ class SmartHomeLayout extends LitElement {
 	}
 
 	render() {
+    		if (this.activeView.type === "noconfig") {
+        		window.location.hash = `#/${state.config.views[0].url}`;
+    		}
 		return html`
 			<mwc-drawer type="modal">
 				<div id="drawer-content">
diff --git a/src/power-meter/live.js b/src/power-meter/live.js
@@ -52,7 +52,7 @@ class PowerMeterLive extends LitElement {
 							<div class="table-column">Total Import</div>
 							<div class="table-column">Import</div>
 						</div>
-			      ${config.meters.map(d => html`
+						${config.meters.map(d => html`
 							<div class="table-row">
 								<div class="table-column">${d.name}</div>
 								<div class="table-column">${round(state.data[d.device].voltage, 2)} V</div>
diff --git a/src/state.js b/src/state.js
@@ -4,7 +4,16 @@ class State {
 	constructor() {
 		this.connected = false;
 		this.data = JSON.parse(localStorage.getItem("data") || "{}");
-		this.config = JSON.parse(localStorage.getItem("config") || "{}");
+		this.config = JSON.parse(localStorage.getItem("config") || JSON.stringify({
+    			views: [
+        			{
+            				type: "noconfig",
+            				url: "noconfig",
+            				name: "Start",
+            				icon: "settings"
+        			}
+    			]
+    		}));
 		this.data.lastUpdated = new Date(this.data.lastUpdated);
 		this._subscribers = [];
 		this._initWS();
diff --git a/src/temperature/history.js b/src/temperature/history.js
@@ -0,0 +1,196 @@
+"use strict";
+
+import { LitElement, html, css } from "lit-element";
+import { state } from "../state.js";
+import { pad, round, weekdays, months, get } from "../util.js";
+import { archive } from "../archive.js";
+
+import "@authentic/mwc-icon";
+import "@authentic/mwc-select";
+import "@authentic/mwc-menu";
+import "@authentic/mwc-list";
+import "../card.js";
+import "../spinner.js";
+
+const formatName = (type, name) => {
+	if (type == "day") {
+		const date = new Date(`${name.substr(0, 4)}-${name.substr(4, 2)}-${name.substr(6, 2)}`);
+		return `${weekdays[date.getDay()]} ${date.getDate()}th`;
+	} else if (type == "week") {
+		let res = "";
+		const date = new Date(name.substr(0, 4), 0, 1 + 7 * (Number(name.substr(4, 2)) - 1));
+		date.setDate(date.getDate() + 1 - date.getDay());
+		res += `${date.getDate()}.${date.getMonth()+1}.`;
+		date.setDate(date.getDate() + 7 - date.getDay());
+		res += ` - ${date.getDate()}.${date.getMonth()+1}.`;
+		return res;
+	} else if (type == "month") {
+		const date = new Date(`${name.substr(0, 4)}-${name.substr(4, 2)}-01`);
+		return months[date.getMonth()];
+	}
+};
+
+class TemperatureHistory extends LitElement {
+	constructor() {
+		super(...arguments);
+		this.type = "day";
+		const now = new Date();
+		this.year = now.getFullYear();
+		this.month = pad(now.getMonth() + 1, 2);
+		this.data = {};
+
+		state.subscribe(this.requestUpdate.bind(this));
+		archive.subscribe(this.requestUpdate.bind(this));
+	}
+
+	handleSelected(field) {
+		return (evt) => {
+			this[field] = evt.detail.item.value;
+			this.requestUpdate();
+		};
+	}
+
+	render() {
+		const config = state.config.views.filter(view => view.url == this.viewid)[0];
+		const now = new Date();
+
+		this.data = {};
+		this.sel = this.year;
+		if (this.type === "day") this.sel += "_" + this.month;
+
+		archive.load("metadata");
+		const years = get(archive.data, [ "metadata", "availableData" ], {});
+		const months = get(archive.data, [ "metadata", "availableData", this.year ], []);
+
+		console.log(config)
+		for (let d of config.meters) {
+			archive.load(`${this.type}/${d.device}_${this.sel}`);
+			const data = get(archive.data, [ `${this.type}/${d.device}_${this.sel}` ], {});
+			for (let day of Object.keys(data)) {
+				if (!this.data[day]) this.data[day] = {};
+				this.data[day][d.device] = data[day].imported;
+			}
+		}
+
+		return html`
+			<div id="selection" class="${Object.keys(years).length > 0 ? "" : "hidden"}">
+				<mwc-select label="Type">
+					<mwc-menu slot="menu" class="type-sel" @MDCMenu:selected=${this.handleSelected("type")}>
+						<mwc-list>
+							<mwc-list-item value="day">Days</mwc-list-item>
+							<mwc-list-item value="week">Weeks</mwc-list-item>
+							<mwc-list-item value="month">Months</mwc-list-item>
+						</mwc-list>
+					</mwc-menu>
+				</mwc-select>
+				<mwc-select label="Year">
+					<mwc-menu slot="menu" class="year-sel" @MDCMenu:selected=${this.handleSelected("year")}>
+						<mwc-list>
+							${Object.keys(years).map(m => html`<mwc-list-item value="${m}">${m}</mwc-list-item>`)}
+						</mwc-list>
+					</mwc-menu>
+				</mwc-select>
+				<mwc-select label="Month" class="${this.type == "day" ? "" : "hidden"}">
+					<mwc-menu slot="menu" class="month-sel" @MDCMenu:selected=${this.handleSelected("month")}>
+						<mwc-list>
+							${months.map(m => html`<mwc-list-item value="${m}">${Number(m)}</mwc-list-item>`)}
+						</mwc-list>
+					</mwc-menu>
+				</mwc-select>
+			</div>
+			${archive.finishedLoading() ? html`
+				<smarthome-card>
+					<div class="table">
+						<div class="table-row table-head">
+							<div class="table-column">Name</div>
+							${Object.keys(this.data).reverse().map((day) => html`
+								<div class="table-column">${formatName(this.type, day)}</table-column>
+							`)}
+						</div>
+						${config.meters.map(d => html`
+							<div class="table-row">
+								<div class="table-column">${d.name}</div>
+								${Object.entries(this.data).reverse().map(([day, data]) => html`
+									<div class="table-column">${data[d.device] ? round(data[d.device], 2) + " kWh" : "-"}</div>
+								`)}
+							</div>
+						`)}
+					</div>
+				</smarthome-card>
+			` : html`
+			  <smarthome-spinner>Loading...</smarthome-spinner>
+			`}
+		`;
+	}
+
+	static get styles() {
+		return css`
+			:host {
+				display: flex;
+				flex-direction: column;
+				--mdc-theme-on-primary: white;
+				--mdc-theme-primary: #43a047;
+				--mdc-theme-on-secondary: white;
+				--mdc-theme-secondary: #616161;
+			}
+			.table {
+				display: flex;
+				flex-direction: row;
+			}
+			.table-row {
+				display: flex;
+				flex-direction: column;
+				width: 100%;
+			}
+			.table-column {
+				padding: 1em;
+				height: 1em;
+				display: flex;
+				flex-direction: column;
+				justify-content: center;
+			}
+
+			.table-column {
+				background-color: #ddd;
+			}
+			.table-column:nth-child(2n) {
+				background-color: #fff;
+			}
+
+			.hidden {
+				display: none;
+			}
+			#selection {
+				justify-content: center;
+				display: flex;
+				flex-wrap: wrap;
+				flex-direction: row;
+				width: 450px;
+				max-width: 100%;
+				margin-left: auto;
+				margin-right: auto;
+				margin-top: 20px;
+				margin-bottom: 0px;
+				/*justify-content: space-between;*/
+			}
+			mwc-select {
+				width: 100px;
+				margin-right: 10px;
+				margin-left: 10px;
+			}
+			@media (min-width: 600px) {
+				#selection {
+					margin-bottom: -20px;
+				}
+			}
+		`;
+	}
+
+	static get properties() {
+		return {
+			viewid: { type: String }
+		};
+	}
+}
+
+customElements.define("smarthome-temperature-history", TemperatureHistory);
diff --git a/src/temperature/index.js b/src/temperature/index.js
@@ -0,0 +1,74 @@
+"use strict";
+
+import { LitElement, html, css } from "lit-element";
+
+import "./history.js";
+import "./live.js";
+
+const pages = {
+	"live": { name: "Live", content: id => html`<smarthome-temperature-live .viewid="${id}"></smarthome-temperature-live>` },
+	//"history": { name: "History", content: id => html`<smarthome-temperature-history .viewid="${id}"></smarthome-temperature-history>` },
+};
+
+class Temperature extends LitElement {
+	constructor() {
+		super(...arguments);
+		this.activePage = pages["live"];
+	}
+
+	_setPage(path) {
+		return () => {
+			this.activePage = pages[path];
+			this.requestUpdate();
+		};
+	}
+
+	render() {
+		return html`
+			<div id="pages">
+				${Object.entries(pages).map(([path, page]) => html`
+					<a href="#/temperature" class="page-link ${this.activePage == page ? "active" : "inactive"}" @click=${this._setPage(path)}>
+						${page.name}
+					</a>
+				`)}
+			</div>
+			${this.activePage.content(this.viewid)}
+		`;
+	}
+
+	static get styles() {
+		return css`
+			:host {
+				display: flex;
+				flex-direction: column;
+				--mdc-theme-on-primary: white;
+				--mdc-theme-primary: #43a047;
+				--mdc-theme-on-secondary: white;
+				--mdc-theme-secondary: #616161;
+			}
+
+			#pages {
+				display: flex;
+				flex-direction: row;
+				justify-content: center;
+			}
+			.page-link {
+				padding: .5em;
+				margin: .5em;
+				color: black;
+				text-decoration: none;
+			}
+			.page-link.active {
+				border-bottom: 4px solid #43a047;
+			}
+		`;
+	}
+
+	static get properties() {
+		return {
+			viewid: { type: String }
+		};
+	}
+}
+
+customElements.define("smarthome-temperature", Temperature);
diff --git a/src/temperature/live.js b/src/temperature/live.js
@@ -0,0 +1,132 @@
+"use strict";
+
+import { LitElement, html, css } from "lit-element";
+import { state } from "../state.js";
+import { pad, formatDate, round, get } from "../util.js";
+import { archive } from "../archive.js";
+
+import "@authentic/mwc-icon";
+import "../card.js";
+import "../spinner.js";
+
+class TemperatureLive extends LitElement {
+	constructor() {
+		super(...arguments);
+		this._cachedDayArchive = {};
+		state.subscribe(this.requestUpdate.bind(this));
+		archive.subscribe(this.requestUpdate.bind(this));
+	}
+
+	getImport(d) {
+		const currentValue = state.data[d.device].import;
+		const now = new Date();
+		console.log(d);
+		const filename = `day/${d.device}_${now.getFullYear()}_${pad(now.getMonth() + 1, 2)}`;
+		if (this._cachedDayArchive[filename] !== now.getDate()) delete archive.data[filename];
+		archive.load(`day/${d.device}_${now.getFullYear()}_${pad(now.getMonth() + 1, 2)}`);
+		this._cachedDayArchive[filename] = now.getDate();
+		const lastValue = get(archive.data, [filename, `${now.getFullYear()}${pad(now.getMonth() + 1, 2)}${pad(now.getDate() - 1, 2)}`, "totalImported" ]);
+		return currentValue && lastValue ? round(currentValue - lastValue, 2) + " kWh" : "-";
+	}
+
+	render() {
+		const config = state.config.views.filter(view => view.url == this.viewid)[0];
+		return html`
+			${state.data[config.sensors[0].device] ? html`
+				<div id="connection-status">
+					${state.connected ? html`
+						<p><mwc-icon>cloud_queue</mwc-icon> Connected</p>
+					` : html`
+						<p><mwc-icon>cloud_off</mwc-icon> Disconnected</p>
+						<p class="lastupdate">last updated ${formatDate(state.data.lastUpdated)}</p>
+					`}
+				</div>
+				<smarthome-card>
+					<div class="table">
+						<div class="table-row table-head">
+							<div class="table-column">Name</div>
+							<div class="table-column">Temperature</div>
+							<div class="table-column">Humidity</div>
+						</div>
+						${config.sensors.map(d => html`
+							<div class="table-row">
+								<div class="table-column">${d.name}</div>
+								<div class="table-column">${round(state.data[d.device].temperature, 2)}°C</div>
+								<div class="table-column">${round(state.data[d.device].humidity, 2)}%</div>
+							</div>
+						`)}
+					</div>
+				</smarthome-card>
+			` : html`
+				<smarthome-spinner>Trying to connect...</smarthome-spinner>
+			`}
+		`;
+	}
+
+	static get styles() {
+		return css`
+			:host {
+				display: flex;
+				flex-direction: column;
+				--mdc-theme-on-primary: white;
+				--mdc-theme-primary: #43a047;
+				--mdc-theme-on-secondary: white;
+				--mdc-theme-secondary: #616161;
+			}
+			#connection-status mwc-icon {
+				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;
+			}
+			.table-row {
+				display: flex;
+				flex-direction: column;
+				width: 100%;
+			}
+			.table-column {
+				padding: 1em;
+				height: 1em;
+				display: flex;
+				flex-direction: column;
+				justify-content: center;
+			}
+
+			.table-column {
+				background-color: #ddd;
+			}
+			.table-column:nth-child(2n) {
+				background-color: #fff;
+			}
+			@media (min-width: 600px) {
+				#connection-status {
+					margin-top: 20px;
+				}
+			}
+		`;
+	}
+
+	static get properties() {
+		return {
+			viewid: { type: String }
+		};
+	}
+}
+
+customElements.define("smarthome-temperature-live", TemperatureLive);