From c4d8aba5c8fbcfe48a0e36f396e4b3d7cf9da952 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 17 Aug 2020 13:54:03 -0400 Subject: [PATCH] Add OZW Refresh Node Dialog (#6530) --- src/data/ozw.ts | 97 ++++++- .../ozw/ha-device-info-ozw.ts | 48 +++- .../ozw/dialog-ozw-refresh-node.ts | 272 ++++++++++++++++++ .../ozw/show-dialog-ozw-refresh-node.ts | 22 ++ src/translations/en.json | 35 ++- 5 files changed, 459 insertions(+), 15 deletions(-) create mode 100644 src/panels/config/integrations/integration-panels/ozw/dialog-ozw-refresh-node.ts create mode 100644 src/panels/config/integrations/integration-panels/ozw/show-dialog-ozw-refresh-node.ts diff --git a/src/data/ozw.ts b/src/data/ozw.ts index 491b0168a2..cbef2a8dbc 100644 --- a/src/data/ozw.ts +++ b/src/data/ozw.ts @@ -1,4 +1,10 @@ import { HomeAssistant } from "../types"; +import { DeviceRegistryEntry } from "./device_registry"; + +export interface OZWNodeIdentifiers { + ozw_instance: number; + node_id: number; +} export interface OZWDevice { node_id: number; @@ -7,15 +13,102 @@ export interface OZWDevice { is_failed: boolean; is_zwave_plus: boolean; ozw_instance: number; + event: string; } +export interface OZWDeviceMetaDataResponse { + node_id: number; + ozw_instance: number; + metadata: OZWDeviceMetaData; +} + +export interface OZWDeviceMetaData { + OZWInfoURL: string; + ZWAProductURL: string; + ProductPic: string; + Description: string; + ProductManualURL: string; + ProductPageURL: string; + InclusionHelp: string; + ExclusionHelp: string; + ResetHelp: string; + WakeupHelp: string; + ProductSupportURL: string; + Frequency: string; + Name: string; + ProductPicBase64: string; +} + +export const nodeQueryStages = [ + "ProtocolInfo", + "Probe", + "WakeUp", + "ManufacturerSpecific1", + "NodeInfo", + "NodePlusInfo", + "ManufacturerSpecific2", + "Versions", + "Instances", + "Static", + "CacheLoad", + "Associations", + "Neighbors", + "Session", + "Dynamic", + "Configuration", + "Complete", +]; + +export const getIdentifiersFromDevice = function ( + device: DeviceRegistryEntry +): OZWNodeIdentifiers | undefined { + if (!device) { + return undefined; + } + + const ozwIdentifier = device.identifiers.find( + (identifier) => identifier[0] === "ozw" + ); + if (!ozwIdentifier) { + return undefined; + } + + const identifiers = ozwIdentifier[1].split("."); + return { + node_id: parseInt(identifiers[1]), + ozw_instance: parseInt(identifiers[0]), + }; +}; + export const fetchOZWNodeStatus = ( hass: HomeAssistant, - ozw_instance: string, - node_id: string + ozw_instance: number, + node_id: number ): Promise => hass.callWS({ type: "ozw/node_status", ozw_instance: ozw_instance, node_id: node_id, }); + +export const fetchOZWNodeMetadata = ( + hass: HomeAssistant, + ozw_instance: number, + node_id: number +): Promise => + hass.callWS({ + type: "ozw/node_metadata", + ozw_instance: ozw_instance, + node_id: node_id, + }); + +export const refreshNodeInfo = ( + hass: HomeAssistant, + ozw_instance: number, + node_id: number +): Promise => + hass.callWS({ + type: "ozw/refresh_node_info", + ozw_instance: ozw_instance, + node_id: node_id, + }); diff --git a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts b/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts index b82ba363f0..2197fe689b 100644 --- a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts +++ b/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts @@ -12,7 +12,13 @@ import { import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; import { haStyle } from "../../../../../../resources/styles"; import { HomeAssistant } from "../../../../../../types"; -import { OZWDevice, fetchOZWNodeStatus } from "../../../../../../data/ozw"; +import { + OZWDevice, + fetchOZWNodeStatus, + getIdentifiersFromDevice, + OZWNodeIdentifiers, +} from "../../../../../../data/ozw"; +import { showOZWRefreshNodeDialog } from "../../../../integrations/integration-panels/ozw/show-dialog-ozw-refresh-node"; @customElement("ha-device-info-ozw") export class HaDeviceInfoOzw extends LitElement { @@ -20,26 +26,34 @@ export class HaDeviceInfoOzw extends LitElement { @property() public device!: DeviceRegistryEntry; + @property() + private node_id = 0; + + @property() + private ozw_instance = 1; + @internalProperty() private _ozwDevice?: OZWDevice; protected updated(changedProperties: PropertyValues) { if (changedProperties.has("device")) { - this._fetchNodeDetails(this.device); + const identifiers: + | OZWNodeIdentifiers + | undefined = getIdentifiersFromDevice(this.device); + if (!identifiers) { + return; + } + this.ozw_instance = identifiers.ozw_instance; + this.node_id = identifiers.node_id; + + this._fetchNodeDetails(); } } - protected async _fetchNodeDetails(device) { - const ozwIdentifier = device.identifiers.find( - (identifier) => identifier[0] === "ozw" - ); - if (!ozwIdentifier) { - return; - } - const identifiers = ozwIdentifier[1].split("."); + protected async _fetchNodeDetails() { this._ozwDevice = await fetchOZWNodeStatus( this.hass, - identifiers[0], - identifiers[1] + this.ozw_instance, + this.node_id ); } @@ -69,9 +83,19 @@ export class HaDeviceInfoOzw extends LitElement { ? this.hass.localize("ui.common.yes") : this.hass.localize("ui.common.no")} + + Refresh Node + `; } + private async _refreshNodeClicked() { + showOZWRefreshNodeDialog(this, { + node_id: this.node_id, + ozw_instance: this.ozw_instance, + }); + } + static get styles(): CSSResult[] { return [ haStyle, diff --git a/src/panels/config/integrations/integration-panels/ozw/dialog-ozw-refresh-node.ts b/src/panels/config/integrations/integration-panels/ozw/dialog-ozw-refresh-node.ts new file mode 100644 index 0000000000..6f86fbc7bc --- /dev/null +++ b/src/panels/config/integrations/integration-panels/ozw/dialog-ozw-refresh-node.ts @@ -0,0 +1,272 @@ +import { + CSSResult, + customElement, + html, + LitElement, + property, + internalProperty, + TemplateResult, + PropertyValues, + css, +} from "lit-element"; +import "../../../../../components/ha-code-editor"; +import "../../../../../components/ha-circular-progress"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; +import { haStyleDialog } from "../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../types"; +import { OZWRefreshNodeDialogParams } from "./show-dialog-ozw-refresh-node"; + +import { + fetchOZWNodeMetadata, + OZWDeviceMetaData, + OZWDevice, + nodeQueryStages, +} from "../../../../../data/ozw"; + +@customElement("dialog-ozw-refresh-node") +class DialogOZWRefreshNode extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @internalProperty() private _node_id?: number; + + @internalProperty() private _ozw_instance = 1; + + @internalProperty() private _nodeMetaData?: OZWDeviceMetaData; + + @internalProperty() private _node?: OZWDevice; + + @internalProperty() private _active = false; + + @internalProperty() private _complete = false; + + private _refreshDevicesTimeoutHandle?: number; + + private _subscribed?: Promise<() => Promise>; + + public disconnectedCallback(): void { + super.disconnectedCallback(); + this._unsubscribe(); + } + + protected updated(changedProperties: PropertyValues): void { + super.update(changedProperties); + if (changedProperties.has("node_id")) { + this._fetchData(); + } + } + + private async _fetchData() { + if (!this._node_id) { + return; + } + const metaDataResponse = await fetchOZWNodeMetadata( + this.hass, + this._ozw_instance, + this._node_id + ); + + this._nodeMetaData = metaDataResponse.metadata; + } + + public async showDialog(params: OZWRefreshNodeDialogParams): Promise { + this._node_id = params.node_id; + this._ozw_instance = params.ozw_instance; + this._fetchData(); + } + + protected render(): TemplateResult { + if (!this._node_id) { + return html``; + } + + return html` + + ${this._complete + ? html` +

+ ${this.hass.localize( + "ui.panel.config.ozw.refresh_node.complete" + )} +

+ + ${this.hass.localize("ui.common.close")} + + ` + : html` + ${this._active + ? html` +
+ +
+

+ + ${this.hass.localize( + "ui.panel.config.ozw.refresh_node.refreshing_description" + )} + +

+ ${this._node + ? html` +

+ ${this.hass.localize( + "ui.panel.config.ozw.refresh_node.node_status" + )}: + ${this._node.node_query_stage} + (${this.hass.localize( + "ui.panel.config.ozw.refresh_node.step" + )} + ${nodeQueryStages.indexOf( + this._node.node_query_stage + ) + 1}/17) +

+

+ + ${this.hass.localize( + "ui.panel.config.ozw.node_query_stages." + + this._node.node_query_stage.toLowerCase() + )} +

+ ` + : ``} +
+
+ ` + : html` + ${this.hass.localize( + "ui.panel.config.ozw.refresh_node.description" + )} +

+ ${this.hass.localize( + "ui.panel.config.ozw.refresh_node.battery_note" + )} +

+ `} + ${this._nodeMetaData?.WakeupHelp !== "" + ? html` + + ${this.hass.localize( + "ui.panel.config.ozw.refresh_node.wakeup_header" + )} + ${this._nodeMetaData!.Name} + +
+ ${this._nodeMetaData!.WakeupHelp} +
+ + ${this.hass.localize( + "ui.panel.config.ozw.refresh_node.wakeup_instructions_source" + )} + +
+ ` + : ""} + ${!this._active + ? html` + + ${this.hass.localize( + "ui.panel.config.ozw.refresh_node.start_refresh_button" + )} + + ` + : html``} + `} +
+ `; + } + + private _startRefresh(): void { + this._subscribe(); + } + + private _handleMessage(message: any): void { + if (message.type === "node_updated") { + this._node = message; + if (message.node_query_stage === "Complete") { + this._unsubscribe(); + this._complete = true; + } + } + } + + private _unsubscribe(): void { + this._active = false; + if (this._refreshDevicesTimeoutHandle) { + clearTimeout(this._refreshDevicesTimeoutHandle); + } + if (this._subscribed) { + this._subscribed.then((unsub) => unsub()); + this._subscribed = undefined; + } + } + + private _subscribe(): void { + if (!this.hass) { + return; + } + this._active = true; + this._subscribed = this.hass.connection.subscribeMessage( + (message) => this._handleMessage(message), + { + type: "ozw/refresh_node_info", + node_id: this._node_id, + ozw_instance: this._ozw_instance, + } + ); + this._refreshDevicesTimeoutHandle = window.setTimeout( + () => this._unsubscribe(), + 120000 + ); + } + + private _close(): void { + this._complete = false; + this._node_id = undefined; + this._node = undefined; + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + blockquote { + display: block; + background-color: #ddd; + padding: 8px; + margin: 8px 0; + font-size: 0.9em; + } + + blockquote em { + font-size: 0.9em; + margin-top: 6px; + } + + .flex-container { + display: flex; + align-items: center; + } + + .flex-container ha-circular-progress { + margin-right: 20px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-ozw-refresh-node": DialogOZWRefreshNode; + } +} diff --git a/src/panels/config/integrations/integration-panels/ozw/show-dialog-ozw-refresh-node.ts b/src/panels/config/integrations/integration-panels/ozw/show-dialog-ozw-refresh-node.ts new file mode 100644 index 0000000000..0c956f9ffe --- /dev/null +++ b/src/panels/config/integrations/integration-panels/ozw/show-dialog-ozw-refresh-node.ts @@ -0,0 +1,22 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; + +export interface OZWRefreshNodeDialogParams { + ozw_instance: number; + node_id: number; +} + +export const loadRefreshNodeDialog = () => + import( + /* webpackChunkName: "dialog-ozw-refresh-node" */ "./dialog-ozw-refresh-node" + ); + +export const showOZWRefreshNodeDialog = ( + element: HTMLElement, + refreshNodeDialogParams: OZWRefreshNodeDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-ozw-refresh-node", + dialogImport: loadRefreshNodeDialog, + dialogParams: refreshNodeDialogParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 48d51fa5aa..41f54a7386 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1066,7 +1066,9 @@ "label": "Repeat", "type_select": "Repeat type", "type": { - "count": { "label": "Count" }, + "count": { + "label": "Count" + }, "while": { "label": "While", "conditions": "While conditions" @@ -1628,6 +1630,37 @@ "zwave_info": "Z-Wave Info", "stage": "Stage", "node_failed": "Node Failed" + }, + "node_query_stages": { + "protocolinfo": "Obtaining basic Z-Wave capabilities of this node from the controller", + "probe": "Checking if the node is awake/alive", + "wakeup": "Setting up support for wakeup queues and messages", + "manufacturerspecific1": "Obtaining manufacturer and product ID codes from the node", + "nodeinfo": "Obtaining supported command classes from the node", + "nodeplusinfo": "Obtaining Z-Wave+ information from the node", + "manufacturerspecific2": "Obtaining additional manufacturer and product ID codes from the node", + "versions": "Obtaining information about firmware and command class versions", + "instances": "Obtaining details about what instances or channels a device supports", + "static": "Obtaining static values from the device", + "cacheload": "Loading information from the OpenZWave cache file. Battery nodes will stay at this stage until the node wakes up.", + "associations": "Refreshing association groups and memberships", + "neighbors": "Obtaining a list of the node's neighbors", + "session": "Obtaining infrequently changing values from the node", + "dynamic": "Obtaining frequently changing values from the node", + "configuration": "Obtaining configuration values from the node", + "complete": "Interview process is complete" + }, + "refresh_node": { + "title": "Refresh Node Information", + "complete": "Node Refresh Complete", + "description": "This will tell OpenZWave to re-interview a node and update the node's command classes, capabilities, and values.", + "battery_note": "If the node is battery powered, be sure to wake it before proceeding", + "wakeup_header": "Wakeup Instructions for", + "wakeup_instructions_source": "Wakeup instructions are sourced from the OpenZWave community device database.", + "start_refresh_button": "Start Refresh", + "refreshing_description": "Refreshing node information...", + "node_status": "Node Status", + "step": "Step" } }, "zha": {