diff --git a/src/data/zwave_js.ts b/src/data/zwave_js.ts index 16751e26b2..3219480313 100644 --- a/src/data/zwave_js.ts +++ b/src/data/zwave_js.ts @@ -178,6 +178,17 @@ export const reinterviewNode = ( } ); +export const healNode = ( + hass: HomeAssistant, + entry_id: string, + node_id: number +): Promise => + hass.callWS({ + type: "zwave_js/heal_node", + entry_id: entry_id, + node_id: node_id, + }); + export const healNetwork = ( hass: HomeAssistant, entry_id: string 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 8695c556b1..461a6ab387 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 @@ -16,6 +16,7 @@ import { import { haStyle } from "../../../../../../resources/styles"; import { HomeAssistant } from "../../../../../../types"; import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node"; +import { showZWaveJSHealNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-heal-node"; @customElement("ha-device-actions-zwave_js") export class HaDeviceActionsZWaveJS extends LitElement { @@ -56,6 +57,9 @@ export class HaDeviceActionsZWaveJS extends LitElement { "ui.panel.config.zwave_js.device_info.reinterview_device" )} + + ${this.hass.localize("ui.panel.config.zwave_js.device_info.heal_node")} + `; } @@ -69,6 +73,17 @@ export class HaDeviceActionsZWaveJS extends LitElement { }); } + private async _healNodeClicked() { + if (!this._nodeId || !this._entryId) { + return; + } + showZWaveJSHealNodeDialog(this, { + entry_id: this._entryId, + node_id: this._nodeId, + device: this.device, + }); + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts new file mode 100644 index 0000000000..9da32c7820 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts @@ -0,0 +1,273 @@ +import "../../../../../components/ha-circular-progress"; +import "@material/mwc-button/mwc-button"; +import "@material/mwc-linear-progress/mwc-linear-progress"; +import { mdiStethoscope, mdiCheckCircle, mdiCloseCircle } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; +import { + DeviceRegistryEntry, + computeDeviceName, +} from "../../../../../data/device_registry"; +import { + fetchNetworkStatus, + healNode, + ZWaveJSNetwork, +} from "../../../../../data/zwave_js"; +import { haStyleDialog } from "../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../types"; +import { ZWaveJSHealNodeDialogParams } from "./show-dialog-zwave_js-heal-node"; + +@customElement("dialog-zwave_js-heal-node") +class DialogZWaveJSHealNode extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private entry_id?: string; + + @state() private node_id?: number; + + @state() private device?: DeviceRegistryEntry; + + @state() private _status?: string; + + @state() private _error?: string; + + public showDialog(params: ZWaveJSHealNodeDialogParams): void { + this.entry_id = params.entry_id; + this.device = params.device; + this.node_id = params.node_id; + this._fetchData(); + } + + public closeDialog(): void { + this.entry_id = undefined; + this._status = undefined; + this.node_id = undefined; + this.device = undefined; + this._error = undefined; + + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this.entry_id || !this.device) { + return html``; + } + + return html` + + ${!this._status + ? html` +
+ +
+

+ ${this.hass.localize( + "ui.panel.config.zwave_js.heal_node.introduction", + { + device: html`${computeDeviceName(this.device, this.hass!)}`, + } + )} +

+
+
+

+ + ${this.hass.localize( + "ui.panel.config.zwave_js.heal_node.traffic_warning" + )} + +

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

+ ${this.hass.localize( + "ui.panel.config.zwave_js.heal_node.in_progress", + { + device: html`${computeDeviceName(this.device, this.hass!)}`, + } + )} +

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

+ ${this.hass.localize( + "ui.panel.config.zwave_js.heal_node.healing_failed", + { + device: html`${computeDeviceName(this.device, this.hass!)}`, + } + )} +

+

+ ${this._error + ? html` ${this._error} ` + : ` + ${this.hass.localize( + "ui.panel.config.zwave_js.heal_node.healing_failed_check_logs" + )} + `} +

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

+ ${this.hass.localize( + "ui.panel.config.zwave_js.heal_node.healing_complete", + { + device: html`${computeDeviceName(this.device, this.hass!)}`, + } + )} +

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

+ ${this.hass.localize( + "ui.panel.config.zwave_js.heal_node.network_heal_in_progress" + )} +

+
+
+ + ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + + ` + : ``} +
+ `; + } + + private async _fetchData(): Promise { + if (!this.hass) { + return; + } + const network: ZWaveJSNetwork = await fetchNetworkStatus( + this.hass!, + this.entry_id! + ); + if (network.controller.is_heal_network_active) { + this._status = "network-healing"; + } + } + + private async _startHeal(): Promise { + if (!this.hass) { + return; + } + this._status = "started"; + try { + this._status = (await healNode(this.hass, this.entry_id!, this.node_id!)) + ? "finished" + : "failed"; + } catch (error) { + this._error = error.message; + this._status = "failed"; + } + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + .success { + color: var(--success-color); + } + + .failed { + color: var(--error-color); + } + + .flex-container { + display: flex; + align-items: center; + } + + ha-svg-icon { + width: 68px; + height: 48px; + } + + ha-svg-icon.introduction { + color: var(--primary-color); + } + + .flex-container ha-svg-icon, + .flex-container ha-circular-progress { + margin-right: 20px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-zwave_js-heal-node": DialogZWaveJSHealNode; + } +} diff --git a/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-heal-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-heal-node.ts new file mode 100644 index 0000000000..646b9f1b3e --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-heal-node.ts @@ -0,0 +1,21 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { DeviceRegistryEntry } from "../../../../../data/device_registry"; + +export interface ZWaveJSHealNodeDialogParams { + entry_id: string; + node_id: number; + device: DeviceRegistryEntry; +} + +export const loadHealNodeDialog = () => import("./dialog-zwave_js-heal-node"); + +export const showZWaveJSHealNodeDialog = ( + element: HTMLElement, + healNodeDialogParams: ZWaveJSHealNodeDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-zwave_js-heal-node", + dialogImport: loadHealNodeDialog, + dialogParams: healNodeDialogParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index c434885e8f..21fd0916bc 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2688,7 +2688,8 @@ "node_status": "Node Status", "node_ready": "Node Ready", "device_config": "Configure Device", - "reinterview_device": "Re-interview Device" + "reinterview_device": "Re-interview Device", + "heal_node": "Heal Node" }, "node_config": { "header": "Z-Wave Device Configuration", @@ -2763,6 +2764,17 @@ "healing_failed": "Healing failed. Additional information may be available in the logs.", "healing_cancelled": "Network healing has been cancelled." }, + "heal_node": { + "title": "Heal a Z-Wave Device", + "introduction": "Tell {device} to update its routes back to the controller. This can help with communication issues if you have recently moved the device or your controller.", + "traffic_warning": "The healing process generates a large amount of traffic on the Z-Wave network. This may cause devices to respond slowly (or not at all) while the heal is in progress.", + "start_heal": "Heal Device", + "healing_failed": "{device} could not be healed.", + "healing_failed_check_logs": "Additional information may be available in the logs.", + "healing_complete": "{device} has been healed.", + "in_progress": "{device} healing is in progress.", + "network_heal_in_progress": "A Z-Wave network heal is already in progress. Please wait for it to finish before healing an individual device." + }, "logs": { "title": "Z-Wave JS Logs", "log_level": "Log Level",