From dc5607f55454c56e706a0b0b9de431abc68aac35 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 24 Aug 2020 13:21:25 -0400 Subject: [PATCH] Beginning pages for OZW Config Panel (#6670) --- src/data/ozw.ts | 67 +++++ src/panels/config/ha-panel-config.ts | 7 + .../integrations/ha-integration-card.ts | 4 + .../ozw/ozw-config-dashboard.ts | 227 ++++++++++++++++ .../ozw/ozw-config-network.ts | 253 ++++++++++++++++++ .../ozw/ozw-config-router.ts | 70 +++++ src/translations/en.json | 44 ++- 7 files changed, 671 insertions(+), 1 deletion(-) create mode 100644 src/panels/config/integrations/integration-panels/ozw/ozw-config-dashboard.ts create mode 100644 src/panels/config/integrations/integration-panels/ozw/ozw-config-network.ts create mode 100644 src/panels/config/integrations/integration-panels/ozw/ozw-config-router.ts diff --git a/src/data/ozw.ts b/src/data/ozw.ts index cbef2a8dbc..b24692c01f 100644 --- a/src/data/ozw.ts +++ b/src/data/ozw.ts @@ -39,6 +39,28 @@ export interface OZWDeviceMetaData { ProductPicBase64: string; } +export interface OZWInstance { + ozw_instance: number; + OZWDaemon_Version: string; + OpenZWave_Version: string; + QTOpenZWave_Version: string; + Status: string; + getControllerPath: string; + homeID: string; +} + +export interface OZWNetworkStatistics { + ozw_instance: number; + node_count: number; + readCnt: number; + writeCnt: number; + ACKCnt: number; + CANCnt: number; + NAKCnt: number; + dropped: number; + retries: number; +} + export const nodeQueryStages = [ "ProtocolInfo", "Probe", @@ -59,6 +81,26 @@ export const nodeQueryStages = [ "Complete", ]; +export const networkOnlineStatuses = [ + "driverAllNodesQueried", + "driverAllNodesQueriedSomeDead", + "driverAwakeNodesQueried", +]; +export const networkStartingStatuses = [ + "starting", + "started", + "Ready", + "driverReady", +]; +export const networkOfflineStatuses = [ + "Offline", + "stopped", + "driverFailed", + "driverReset", + "driverRemoved", + "driverAllNodesOnFire", +]; + export const getIdentifiersFromDevice = function ( device: DeviceRegistryEntry ): OZWNodeIdentifiers | undefined { @@ -80,6 +122,31 @@ export const getIdentifiersFromDevice = function ( }; }; +export const fetchOZWInstances = ( + hass: HomeAssistant +): Promise => + hass.callWS({ + type: "ozw/get_instances", + }); + +export const fetchOZWNetworkStatus = ( + hass: HomeAssistant, + ozw_instance: number +): Promise => + hass.callWS({ + type: "ozw/network_status", + ozw_instance: ozw_instance, + }); + +export const fetchOZWNetworkStatistics = ( + hass: HomeAssistant, + ozw_instance: number +): Promise => + hass.callWS({ + type: "ozw/network_statistics", + ozw_instance: ozw_instance, + }); + export const fetchOZWNodeStatus = ( hass: HomeAssistant, ozw_instance: number, diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index dad0e22ee4..1857338ac7 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -352,6 +352,13 @@ class HaPanelConfig extends HassRouterPage { /* webpackChunkName: "panel-config-mqtt" */ "./integrations/integration-panels/mqtt/mqtt-config-panel" ), }, + ozw: { + tag: "ozw-config-router", + load: () => + import( + /* webpackChunkName: "panel-config-ozw" */ "./integrations/integration-panels/ozw/ozw-config-router" + ), + }, }, }; diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index d88b075a10..fc03a16ddd 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -55,6 +55,10 @@ const integrationsWithPanel = { buttonLocalizeKey: "ui.panel.config.zha.button", path: "/config/zha/dashboard", }, + ozw: { + buttonLocalizeKey: "ui.panel.config.ozw.button", + path: "/config/ozw/dashboard", + }, zwave: { buttonLocalizeKey: "ui.panel.config.zwave.button", path: "/config/zwave", diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-config-dashboard.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-config-dashboard.ts new file mode 100644 index 0000000000..f23d7a7114 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/ozw/ozw-config-dashboard.ts @@ -0,0 +1,227 @@ +import "@polymer/paper-item/paper-icon-item"; +import "@polymer/paper-item/paper-item-body"; +import "@material/mwc-fab"; +import { + css, + CSSResultArray, + customElement, + html, + internalProperty, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { navigate } from "../../../../../common/navigate"; +import "../../../../../components/ha-card"; +import "../../../../../components/ha-icon-next"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant, Route } from "../../../../../types"; +import "../../../ha-config-section"; +import { mdiCircle, mdiCheckCircle, mdiCloseCircle, mdiZWave } from "@mdi/js"; +import "../../../../../layouts/hass-tabs-subpage"; +import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; +import "@material/mwc-button/mwc-button"; +import { + OZWInstance, + fetchOZWInstances, + networkOnlineStatuses, + networkOfflineStatuses, + networkStartingStatuses, +} from "../../../../../data/ozw"; + +export const ozwTabs: PageNavigation[] = []; + +@customElement("ozw-config-dashboard") +class OZWConfigDashboard extends LitElement { + @property({ type: Object }) public hass!: HomeAssistant; + + @property({ type: Object }) public route!: Route; + + @property({ type: Boolean }) public narrow!: boolean; + + @property({ type: Boolean }) public isWide!: boolean; + + @property() public configEntryId?: string; + + @internalProperty() private _instances: OZWInstance[] = []; + + public connectedCallback(): void { + super.connectedCallback(); + if (this.hass) { + this._fetchData(); + } + } + + private async _fetchData() { + this._instances = await fetchOZWInstances(this.hass!); + if (this._instances.length === 1) { + navigate( + this, + `/config/ozw/network/${this._instances[0].ozw_instance}`, + true + ); + } + } + + protected render(): TemplateResult { + return html` + + +
+ ${this.hass.localize("ui.panel.config.ozw.select_instance.header")} +
+ +
+ ${this.hass.localize( + "ui.panel.config.ozw.select_instance.introduction" + )} +
+ ${this._instances.length > 0 + ? html` + ${this._instances.map((instance) => { + let status = "unknown"; + let icon = mdiCircle; + if (networkOnlineStatuses.includes(instance.Status)) { + status = "online"; + icon = mdiCheckCircle; + } + if (networkStartingStatuses.includes(instance.Status)) { + status = "starting"; + } + if (networkOfflineStatuses.includes(instance.Status)) { + status = "offline"; + icon = mdiCloseCircle; + } + + return html` + + + + + + + ${this.hass.localize( + "ui.panel.config.ozw.common.instance" + )} + ${instance.ozw_instance} +
+ + ${this.hass.localize( + "ui.panel.config.ozw.network_status." + status + )} + - + ${this.hass.localize( + "ui.panel.config.ozw.network_status.details." + + instance.Status.toLowerCase() + )}
+ ${this.hass.localize( + "ui.panel.config.ozw.common.controller" + )} + : ${instance.getControllerPath}
+ OZWDaemon ${instance.OZWDaemon_Version} (OpenZWave + ${instance.OpenZWave_Version}) +
+
+ +
+
+
+ `; + })} + ` + : ``} +
+
+ `; + } + + static get styles(): CSSResultArray { + return [ + haStyle, + css` + ha-card:last-child { + margin-bottom: 24px; + } + ha-config-section { + margin-top: -12px; + } + :host([narrow]) ha-config-section { + margin-top: -20px; + } + ha-card { + overflow: hidden; + } + ha-card a { + text-decoration: none; + color: var(--primary-text-color); + } + paper-item-body { + margin: 16px 0; + } + a { + text-decoration: none; + color: var(--primary-text-color); + position: relative; + display: block; + outline: 0; + } + ha-svg-icon.network-status-icon { + height: 14px; + width: 14px; + } + .online { + color: green; + } + .starting { + color: orange; + } + .offline { + color: red; + } + ha-svg-icon, + ha-icon-next { + color: var(--secondary-text-color); + } + .iron-selected paper-item::before, + a:not(.iron-selected):focus::before { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; + content: ""; + transition: opacity 15ms linear; + will-change: opacity; + } + a:not(.iron-selected):focus::before { + background-color: currentColor; + opacity: var(--dark-divider-opacity); + } + .iron-selected paper-item:focus::before, + .iron-selected:focus paper-item::before { + opacity: 0.2; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ozw-config-dashboard": OZWConfigDashboard; + } +} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-config-network.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-config-network.ts new file mode 100644 index 0000000000..6d67ed779e --- /dev/null +++ b/src/panels/config/integrations/integration-panels/ozw/ozw-config-network.ts @@ -0,0 +1,253 @@ +import "@material/mwc-fab"; +import { + css, + CSSResultArray, + customElement, + html, + internalProperty, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { navigate } from "../../../../../common/navigate"; +import "../../../../../components/ha-card"; +import "../../../../../components/ha-icon-next"; +import "../../../../../components/buttons/ha-call-service-button"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant, Route } from "../../../../../types"; +import "../../../ha-config-section"; +import { mdiCircle, mdiCheckCircle, mdiCloseCircle } from "@mdi/js"; +import "../../../../../layouts/hass-tabs-subpage"; +import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; +import "@material/mwc-button/mwc-button"; +import { + OZWInstance, + fetchOZWNetworkStatus, + fetchOZWNetworkStatistics, + networkOnlineStatuses, + networkOfflineStatuses, + networkStartingStatuses, + OZWNetworkStatistics, +} from "../../../../../data/ozw"; + +export const ozwTabs: PageNavigation[] = []; + +@customElement("ozw-config-network") +class OZWConfigNetwork extends LitElement { + @property({ type: Object }) public hass!: HomeAssistant; + + @property({ type: Object }) public route!: Route; + + @property({ type: Boolean }) public narrow!: boolean; + + @property({ type: Boolean }) public isWide!: boolean; + + @property() public configEntryId?: string; + + @property() public ozw_instance = 0; + + @internalProperty() private _network?: OZWInstance; + + @internalProperty() private _statistics?: OZWNetworkStatistics; + + @internalProperty() private _status = "unknown"; + + @internalProperty() private _icon = mdiCircle; + + public connectedCallback(): void { + super.connectedCallback(); + if (this.ozw_instance <= 0) { + navigate(this, "/config/ozw/dashboard", true); + } + if (this.hass) { + this._fetchData(); + } + } + + private async _fetchData() { + this._network = await fetchOZWNetworkStatus(this.hass!, this.ozw_instance); + this._statistics = await fetchOZWNetworkStatistics( + this.hass!, + this.ozw_instance + ); + if (networkOnlineStatuses.includes(this._network.Status)) { + this._status = "online"; + this._icon = mdiCheckCircle; + } + if (networkStartingStatuses.includes(this._network.Status)) { + this._status = "starting"; + } + if (networkOfflineStatuses.includes(this._network.Status)) { + this._status = "offline"; + this._icon = mdiCloseCircle; + } + } + + private _generateServiceButton(service: string) { + return html` + + ${this.hass!.localize("ui.panel.config.ozw.services." + service)} + + `; + } + + protected render(): TemplateResult { + return html` + + +
+ ${this.hass.localize("ui.panel.config.ozw.network.header")} +
+ +
+ ${this.hass.localize("ui.panel.config.ozw.network.introduction")} +
+ ${this._network + ? html` + +
+
+ + ${this.hass.localize( + "ui.panel.config.ozw.common.network" + )} + ${this.hass.localize( + "ui.panel.config.ozw.network_status." + this._status + )} +
+ + ${this.hass.localize( + "ui.panel.config.ozw.network_status.details." + + this._network.Status.toLowerCase() + )} + +
+
+ ${this.hass.localize( + "ui.panel.config.ozw.common.ozw_instance" + )} + ${this._network.ozw_instance} + ${this._statistics + ? html` + • + ${this.hass.localize( + "ui.panel.config.ozw.network.node_count", + "count", + this._statistics.node_count + )} + ` + : ``} +
+ ${this.hass.localize( + "ui.panel.config.ozw.common.controller" + )}: + ${this._network.getControllerPath}
+ OZWDaemon ${this._network.OZWDaemon_Version} (OpenZWave + ${this._network.OpenZWave_Version}) +
+
+
+ ${this._generateServiceButton("add_node")} + ${this._generateServiceButton("remove_node")} +
+
+ ` + : ``} +
+
+ `; + } + + static get styles(): CSSResultArray { + return [ + haStyle, + css` + .secondary { + color: var(--secondary-text-color); + } + .online { + color: green; + } + .starting { + color: orange; + } + .offline { + color: red; + } + .content { + margin-top: 24px; + } + + .sectionHeader { + position: relative; + padding-right: 40px; + } + + .network-status { + text-align: center; + } + + .network-status div.details { + font-size: 1.5rem; + margin-bottom: 16px; + } + + .network-status ha-svg-icon { + display: block; + margin: 0px auto 16px; + width: 48px; + height: 48px; + } + + .network-status small { + font-size: 1rem; + } + + ha-card { + margin: 0 auto; + max-width: 600px; + } + + .card-actions.warning ha-call-service-button { + color: var(--error-color); + } + + .toggle-help-icon { + position: absolute; + top: -6px; + right: 0; + color: var(--primary-color); + } + + ha-service-description { + display: block; + color: grey; + padding: 0 8px 12px; + } + + [hidden] { + display: none; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ozw-config-network": OZWConfigNetwork; + } +} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-config-router.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-config-router.ts new file mode 100644 index 0000000000..28402cbe4c --- /dev/null +++ b/src/panels/config/integrations/integration-panels/ozw/ozw-config-router.ts @@ -0,0 +1,70 @@ +import { customElement, property } from "lit-element"; +import { + HassRouterPage, + RouterOptions, +} from "../../../../../layouts/hass-router-page"; +import { HomeAssistant } from "../../../../../types"; +import { navigate } from "../../../../../common/navigate"; + +@customElement("ozw-config-router") +class OZWConfigRouter extends HassRouterPage { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public isWide!: boolean; + + @property() public narrow!: boolean; + + private _configEntry = new URLSearchParams(window.location.search).get( + "config_entry" + ); + + protected routerOptions: RouterOptions = { + defaultPage: "dashboard", + showLoading: true, + routes: { + dashboard: { + tag: "ozw-config-dashboard", + load: () => + import( + /* webpackChunkName: "ozw-config-dashboard" */ "./ozw-config-dashboard" + ), + }, + network: { + tag: "ozw-config-network", + load: () => + import( + /* webpackChunkName: "ozw-config-network" */ "./ozw-config-network" + ), + }, + }, + }; + + protected updatePageEl(el): void { + el.route = this.routeTail; + el.hass = this.hass; + el.isWide = this.isWide; + el.narrow = this.narrow; + el.configEntryId = this._configEntry; + if (this._currentPage === "network") { + el.ozw_instance = this.routeTail.path.substr(1); + } + + const searchParams = new URLSearchParams(window.location.search); + if (this._configEntry && !searchParams.has("config_entry")) { + searchParams.append("config_entry", this._configEntry); + navigate( + this, + `${this.routeTail.prefix}${ + this.routeTail.path + }?${searchParams.toString()}`, + true + ); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "ozw-config-router": OZWConfigRouter; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index e18f1ec129..7d665900a5 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1684,10 +1684,14 @@ "message_received": "Message {id} received on {topic} at {time}:" }, "ozw": { + "button": "Configure", "common": { "zwave": "Z-Wave", "node_id": "Node ID", - "ozw_instance": "OpenZWave Instance" + "ozw_instance": "OpenZWave Instance", + "instance": "Instance", + "controller": "Controller", + "network": "Network" }, "device_info": { "zwave_info": "Z-Wave Info", @@ -1724,6 +1728,44 @@ "refreshing_description": "Refreshing node information...", "node_status": "Node Status", "step": "Step" + }, + "network_status": { + "online": "Online", + "offline": "Offline", + "starting": "Starting", + "unknown": "Unknown", + "details": { + "driverallnodesqueried": "All nodes have been queried", + "driverallnodesqueriedsomedead": "All nodes have been queried. Some nodes were found dead", + "driverawakenodesqueries": "All awake nodes have been queried", + "driverremoved": "The driver has been removed", + "driverreset": "The driver has been reset", + "driverfailed": "Failed to connect to Z-Wave controller", + "driverready": "Initializing the Z-Wave controller", + "ready": "Ready to connect", + "stopped": "OpenZWave stopped", + "started": "Connected to MQTT", + "starting": "Connecting to MQTT", + "offline": "OZWDaemon offline" + } + }, + "navigation": { + "select_instance": "Select Instance", + "network": "Network", + "nodes": "Nodes" + }, + "select_instance": { + "header": "Select an OpenZWave Instance", + "introduction": "You have more than one OpenZWave instance running. Which instance would you like to manage?" + }, + "network": { + "header": "Network Management", + "introduction": "Manage network-wide functions.", + "node_count": "{count} nodes" + }, + "services": { + "add_node": "Add Node", + "remove_node": "Remove Node" } }, "zha": {