diff --git a/src/data/zwave_js.ts b/src/data/zwave_js.ts index f346a5e8a1..e58d0c4331 100644 --- a/src/data/zwave_js.ts +++ b/src/data/zwave_js.ts @@ -1,3 +1,4 @@ +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { HomeAssistant } from "../types"; import { DeviceRegistryEntry } from "./device_registry"; @@ -71,6 +72,11 @@ export interface ZWaveJSDataCollectionStatus { opted_in: boolean; } +export interface ZWaveJSRefreshNodeStatusMessage { + event: string; + stage?: string; +} + export enum NodeStatus { Unknown, Asleep, @@ -151,6 +157,22 @@ export const setNodeConfigParameter = ( return hass.callWS(data); }; +export const reinterviewNode = ( + hass: HomeAssistant, + entry_id: string, + node_id: number, + callbackFunction: (message: ZWaveJSRefreshNodeStatusMessage) => void +): Promise => { + return hass.connection.subscribeMessage( + (message: any) => callbackFunction(message), + { + type: "zwave_js/refresh_node_info", + entry_id: entry_id, + node_id: node_id, + } + ); +}; + export const getIdentifiersFromDevice = function ( device: DeviceRegistryEntry ): ZWaveJSNodeIdentifiers | undefined { diff --git a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts index 13c7995243..a43d8126f3 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts @@ -11,9 +11,13 @@ import { TemplateResult, } from "lit-element"; import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; +import { + getIdentifiersFromDevice, + ZWaveJSNodeIdentifiers, +} from "../../../../../../data/zwave_js"; import { haStyle } from "../../../../../../resources/styles"; - import { HomeAssistant } from "../../../../../../types"; +import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node"; @customElement("ha-device-actions-zwave_js") export class HaDeviceActionsZWaveJS extends LitElement { @@ -23,9 +27,19 @@ export class HaDeviceActionsZWaveJS extends LitElement { @internalProperty() private _entryId?: string; + @internalProperty() private _nodeId?: number; + protected updated(changedProperties: PropertyValues) { if (changedProperties.has("device")) { this._entryId = this.device.config_entries[0]; + + const identifiers: + | ZWaveJSNodeIdentifiers + | undefined = getIdentifiersFromDevice(this.device); + if (!identifiers) { + return; + } + this._nodeId = identifiers.node_id; } } @@ -40,9 +54,22 @@ export class HaDeviceActionsZWaveJS extends LitElement { )} + Re-interview Device `; } + private async _reinterviewClicked() { + if (!this._nodeId || !this._entryId) { + return; + } + showZWaveJSReinterviewNodeDialog(this, { + entry_id: this._entryId, + node_id: this._nodeId, + }); + } + static get styles(): CSSResult[] { return [ haStyle, diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-reinterview-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-reinterview-node.ts new file mode 100644 index 0000000000..7dfc9a999a --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-reinterview-node.ts @@ -0,0 +1,262 @@ +import "@material/mwc-button/mwc-button"; +import { mdiCheckCircle, mdiCloseCircle } from "@mdi/js"; +import { + CSSResult, + customElement, + html, + LitElement, + property, + internalProperty, + TemplateResult, + css, +} from "lit-element"; +import "../../../../../components/ha-circular-progress"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; +import { haStyleDialog } from "../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../types"; +import { ZWaveJSReinterviewNodeDialogParams } from "./show-dialog-zwave_js-reinterview-node"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { reinterviewNode } from "../../../../../data/zwave_js"; + +@customElement("dialog-zwave_js-reinterview-node") +class DialogZWaveJSReinterviewNode extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @internalProperty() private entry_id?: string; + + @internalProperty() private node_id?: number; + + @internalProperty() private _status?: string; + + @internalProperty() private _stages?: string[]; + + private _subscribed?: Promise; + + public async showDialog( + params: ZWaveJSReinterviewNodeDialogParams + ): Promise { + this._stages = undefined; + this.entry_id = params.entry_id; + this.node_id = params.node_id; + } + + protected render(): TemplateResult { + if (!this.entry_id) { + return html``; + } + + return html` + + ${!this._status + ? html` +

+ ${this.hass.localize( + "ui.panel.config.zwave_js.reinterview_node.introduction" + )} +

+

+ + ${this.hass.localize( + "ui.panel.config.zwave_js.reinterview_node.battery_device_warning" + )} + +

+ + ${this.hass.localize( + "ui.panel.config.zwave_js.reinterview_node.start_reinterview" + )} + + ` + : ``} + ${this._status === "started" + ? html` +
+ +
+

+ + ${this.hass.localize( + "ui.panel.config.zwave_js.reinterview_node.in_progress" + )} + +

+

+ ${this.hass.localize( + "ui.panel.config.zwave_js.reinterview_node.run_in_background" + )} +

+
+
+ + ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + + ` + : ``} + ${this._status === "failed" + ? html` +
+ +
+

+ ${this.hass.localize( + "ui.panel.config.zwave_js.reinterview_node.interview_failed" + )} +

+
+
+ + ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + + ` + : ``} + ${this._status === "finished" + ? html` +
+ +
+

+ ${this.hass.localize( + "ui.panel.config.zwave_js.reinterview_node.interview_complete" + )} +

+
+
+ + ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + + ` + : ``} + ${this._stages + ? html` +
+ ${this._stages.map( + (stage) => html` + + + ${stage} + + ` + )} +
+ ` + : ""} +
+ `; + } + + private _startReinterview(): void { + if (!this.hass) { + return; + } + this._subscribed = reinterviewNode( + this.hass, + this.entry_id!, + this.node_id!, + this._handleMessage.bind(this) + ); + } + + private _handleMessage(message: any): void { + if (message.event === "interview started") { + this._status = "started"; + } + if (message.event === "interview stage completed") { + if (this._stages === undefined) { + this._stages = [message.stage]; + } else { + this._stages = [...this._stages, message.stage]; + } + } + if (message.event === "interview failed") { + this._unsubscribe(); + this._status = "failed"; + } + if (message.event === "interview completed") { + this._unsubscribe(); + this._status = "finished"; + } + } + + private _unsubscribe(): void { + if (this._subscribed) { + this._subscribed.then((unsub) => unsub()); + this._subscribed = undefined; + } + } + + public closeDialog(): void { + this.entry_id = undefined; + this.node_id = undefined; + this._status = undefined; + this._stages = undefined; + + this._unsubscribe(); + + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + .success { + color: var(--success-color); + } + + .failed { + color: var(--warning-color); + } + + .flex-container { + display: flex; + align-items: center; + } + + .stages { + margin-top: 16px; + } + + .stage ha-svg-icon { + width: 16px; + height: 16px; + } + .stage { + padding: 8px; + } + + ha-svg-icon { + width: 68px; + height: 48px; + } + + .flex-container ha-circular-progress, + .flex-container ha-svg-icon { + margin-right: 20px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-zwave_js-reinterview-node": DialogZWaveJSReinterviewNode; + } +} diff --git a/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node.ts new file mode 100644 index 0000000000..7755786750 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node.ts @@ -0,0 +1,20 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; + +export interface ZWaveJSReinterviewNodeDialogParams { + entry_id: string; + node_id: number; +} + +export const loadReinterviewNodeDialog = () => + import("./dialog-zwave_js-reinterview-node"); + +export const showZWaveJSReinterviewNodeDialog = ( + element: HTMLElement, + reinterviewNodeDialogParams: ZWaveJSReinterviewNodeDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-zwave_js-reinterview-node", + dialogImport: loadReinterviewNodeDialog, + dialogParams: reinterviewNodeDialogParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index bffd90913f..007a729ede 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2649,6 +2649,16 @@ "follow_device_instructions": "Follow the directions that came with your device to trigger exclusion on the device.", "exclusion_failed": "The node could not be removed. Please check the logs for more information.", "exclusion_finished": "Node {id} has been removed from your Z-Wave network." + }, + "reinterview_node": { + "title": "Re-interview a Z-Wave Device", + "introduction": "Re-interview a device on your Z-Wave network. Use this feature if your device has missing or incorrect functionality.", + "battery_device_warning": "You will need to wake battery powered devices before starting the re-interview. Refer to your device's manual for instructions on how to wake the device.", + "run_in_background": "You can close this dialog and the interview will continue in the background.", + "start_reinterview": "Start Re-interview", + "in_progress": "The device is being interviewed. This may take some time.", + "interview_failed": "The device interview failed. Additional information may be available in the logs.", + "interview_complete": "Device interview complete." } } },