diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts new file mode 100644 index 0000000000..fa9f30869e --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts @@ -0,0 +1,307 @@ +import "@material/mwc-button/mwc-button"; +import { mdiCheckCircle, mdiCloseCircle } from "@mdi/js"; +import "../../../../../components/ha-switch"; +import "../../../../../components/ha-formfield"; +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 { ZWaveJSAddNodeDialogParams } from "./show-dialog-zwave_js-add-node"; +import { fireEvent } from "../../../../../common/dom/fire_event"; + +export interface ZWaveJSAddNodeDevice { + id: string; + name: string; +} + +@customElement("dialog-zwave_js-add-node") +class DialogZWaveJSAddNode extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @internalProperty() private entry_id?: string; + + @internalProperty() private _use_secure_inclusion = false; + + @internalProperty() private _status = ""; + + @internalProperty() private _device?: ZWaveJSAddNodeDevice; + + private _addNodeTimeoutHandle?: number; + + private _subscribed?: Promise<() => Promise>; + + public disconnectedCallback(): void { + super.disconnectedCallback(); + this._unsubscribe(); + } + + public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise { + this.entry_id = params.entry_id; + } + + protected render(): TemplateResult { + if (!this.entry_id) { + return html``; + } + + return html` + + ${this._status === "" + ? html` +

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

+
+ + + +

+ + + ${this.hass!.localize( + "ui.panel.config.zwave_js.add_node.secure_inclusion_warning" + )} + + +

+
+ + ${this._use_secure_inclusion + ? html`${this.hass.localize( + "ui.panel.config.zwave_js.add_node.start_secure_inclusion" + )}` + : html` ${this.hass.localize( + "ui.panel.config.zwave_js.add_node.start_inclusion" + )}`} + + ` + : ``} + ${this._status === "started" + ? html` +
+ +
+

+ ${this.hass.localize( + "ui.panel.config.zwave_js.add_node.controller_in_inclusion_mode" + )} +

+

+ ${this.hass.localize( + "ui.panel.config.zwave_js.add_node.follow_device_instructions" + )} +

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

+ ${this.hass.localize( + "ui.panel.config.zwave_js.add_node.inclusion_failed" + )} +

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

+ ${this.hass.localize( + "ui.panel.config.zwave_js.add_node.inclusion_finished" + )} +

+ + + ${this.hass.localize( + "ui.panel.config.zwave_js.add_node.view_device" + )} + + +
+
+ + ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + + ` + : ``} +
+ `; + } + + private async _secureInclusionToggleChanged(ev): Promise { + const target = ev.target; + this._use_secure_inclusion = target.checked; + } + + private _startInclusion(): void { + if (!this.hass) { + return; + } + this._subscribed = this.hass.connection.subscribeMessage( + (message) => this._handleMessage(message), + { + type: "zwave_js/add_node", + entry_id: this.entry_id, + secure: this._use_secure_inclusion, + } + ); + this._addNodeTimeoutHandle = window.setTimeout( + () => this._unsubscribe(), + 90000 + ); + } + + private _handleMessage(message: any): void { + if (message.event === "inclusion started") { + this._status = "started"; + } + if (message.event === "inclusion failed") { + this._unsubscribe(); + this._status = "failed"; + } + if (message.event === "inclusion stopped") { + if (this._status !== "finished") { + this._status = ""; + } + this._unsubscribe(); + } + if (message.event === "device registered") { + this._device = message.device; + this._status = "finished"; + this._unsubscribe(); + } + } + + private _unsubscribe(): void { + if (this._subscribed) { + this._subscribed.then((unsub) => unsub()); + this._subscribed = undefined; + } + if (this._status === "started") { + this.hass.callWS({ + type: "zwave_js/stop_inclusion", + entry_id: this.entry_id, + }); + } + if (this._status !== "finished") { + this._status = ""; + } + if (this._addNodeTimeoutHandle) { + clearTimeout(this._addNodeTimeoutHandle); + } + } + + public closeDialog(): void { + this._unsubscribe(); + this.entry_id = undefined; + this._status = ""; + this._device = undefined; + this._use_secure_inclusion = false; + + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + .secure_inclusion_field { + margin-top: 48px; + } + + .success { + color: green; + } + + .failed { + color: red; + } + + 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; + } + + 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-add-node": DialogZWaveJSAddNode; + } +} diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-node.ts new file mode 100644 index 0000000000..a319ad8ce9 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-node.ts @@ -0,0 +1,264 @@ +import "@material/mwc-button/mwc-button"; +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 { ZWaveJSRemoveNodeDialogParams } from "./show-dialog-zwave_js-remove-node"; +import { mdiCheckCircle, mdiCloseCircle } from "@mdi/js"; +import { fireEvent } from "../../../../../common/dom/fire_event"; + +export interface ZWaveJSRemovedNode { + node_id: number; + manufacturer: string; + label: string; +} + +@customElement("dialog-zwave_js-remove-node") +class DialogZWaveJSRemoveNode extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @internalProperty() private entry_id?: string; + + @internalProperty() private _status = ""; + + @internalProperty() private _node?: ZWaveJSRemovedNode; + + private _removeNodeTimeoutHandle?: number; + + private _subscribed?: Promise<() => Promise>; + + public disconnectedCallback(): void { + super.disconnectedCallback(); + this._unsubscribe(); + } + + public async showDialog( + params: ZWaveJSRemoveNodeDialogParams + ): Promise { + this.entry_id = params.entry_id; + } + + protected render(): TemplateResult { + if (!this.entry_id) { + return html``; + } + + return html` + + ${this._status === "" + ? html` +

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

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

+ ${this.hass.localize( + "ui.panel.config.zwave_js.remove_node.controller_in_exclusion_mode" + )} +

+

+ ${this.hass.localize( + "ui.panel.config.zwave_js.remove_node.follow_device_instructions" + )} +

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

+ ${this.hass.localize( + "ui.panel.config.zwave_js.remove_node.exclusion_failed" + )} +

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

+ ${this.hass.localize( + "ui.panel.config.zwave_js.remove_node.exclusion_finished", + "id", + this._node!.node_id + )} +

+
+
+ + ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + + ` + : ``} +
+ `; + } + + private _startExclusion(): void { + if (!this.hass) { + return; + } + this._subscribed = this.hass.connection.subscribeMessage( + (message) => this._handleMessage(message), + { + type: "zwave_js/remove_node", + entry_id: this.entry_id, + } + ); + this._removeNodeTimeoutHandle = window.setTimeout( + () => this._unsubscribe(), + 120000 + ); + } + + private _handleMessage(message: any): void { + if (message.event === "exclusion started") { + this._status = "started"; + } + if (message.event === "exclusion failed") { + this._unsubscribe(); + this._status = "failed"; + } + if (message.event === "exclusion stopped") { + if (this._status !== "finished") { + this._status = ""; + } + this._unsubscribe(); + } + if (message.event === "node removed") { + this._status = "finished"; + this._node = message.node; + this._unsubscribe(); + } + } + + private _unsubscribe(): void { + if (this._subscribed) { + this._subscribed.then((unsub) => unsub()); + this._subscribed = undefined; + } + if (this._status === "started") { + this.hass.callWS({ + type: "zwave_js/stop_exclusion", + entry_id: this.entry_id, + }); + } + if (this._status !== "finished") { + this._status = ""; + } + if (this._removeNodeTimeoutHandle) { + clearTimeout(this._removeNodeTimeoutHandle); + } + } + + public closeDialog(): void { + this._unsubscribe(); + this.entry_id = undefined; + this._status = ""; + + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + .success { + color: green; + } + + .failed { + color: red; + } + + 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; + } + + 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-remove-node": DialogZWaveJSRemoveNode; + } +} diff --git a/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node.ts new file mode 100644 index 0000000000..702a618fa3 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node.ts @@ -0,0 +1,18 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; + +export interface ZWaveJSAddNodeDialogParams { + entry_id: string; +} + +export const loadAddNodeDialog = () => import("./dialog-zwave_js-add-node"); + +export const showZWaveJSAddNodeDialog = ( + element: HTMLElement, + addNodeDialogParams: ZWaveJSAddNodeDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-zwave_js-add-node", + dialogImport: loadAddNodeDialog, + dialogParams: addNodeDialogParams, + }); +}; diff --git a/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-node.ts new file mode 100644 index 0000000000..ac76e6dbc8 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-node.ts @@ -0,0 +1,19 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; + +export interface ZWaveJSRemoveNodeDialogParams { + entry_id: string; +} + +export const loadRemoveNodeDialog = () => + import("./dialog-zwave_js-remove-node"); + +export const showZWaveJSRemoveNodeDialog = ( + element: HTMLElement, + removeNodeDialogParams: ZWaveJSRemoveNodeDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-zwave_js-remove-node", + dialogImport: loadRemoveNodeDialog, + dialogParams: removeNodeDialogParams, + }); +}; diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts index 17297a0402..531ae71988 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts @@ -21,6 +21,8 @@ import "../../../../../layouts/hass-tabs-subpage"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant, Route } from "../../../../../types"; import "../../../ha-config-section"; +import { showZWaveJSAddNodeDialog } from "./show-dialog-zwave_js-add-node"; +import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node"; import { configTabs } from "./zwave_js-config-router"; @customElement("zwave_js-config-dashboard") @@ -137,6 +139,16 @@ class ZWaveJSConfigDashboard extends LitElement { )} + + ${this.hass.localize( + "ui.panel.config.zwave_js.common.add_node" + )} + + + ${this.hass.localize( + "ui.panel.config.zwave_js.common.remove_node" + )} + ` @@ -155,6 +167,18 @@ class ZWaveJSConfigDashboard extends LitElement { } } + private async _addNodeClicked() { + showZWaveJSAddNodeDialog(this, { + entry_id: this.configEntryId!, + }); + } + + private async _removeNodeClicked() { + showZWaveJSRemoveNodeDialog(this, { + entry_id: this.configEntryId!, + }); + } + static get styles(): CSSResultArray { return [ haStyle, diff --git a/src/translations/en.json b/src/translations/en.json index 8352f1a64b..23d5dc807c 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2387,7 +2387,10 @@ "common": { "network": "Network", "node_id": "Node ID", - "home_id": "Home ID" + "home_id": "Home ID", + "close": "Close", + "add_node": "Add Node", + "remove_node": "Remove Node" }, "dashboard": { "header": "Manage your Z-Wave Network", @@ -2413,6 +2416,30 @@ "connected": "Connected", "connecting": "Connecting", "unknown": "Unknown" + }, + "add_node": { + "title": "Add a Z-Wave Node", + "introduction": "This wizard will guide you through adding a node to your Z-Wave network.", + "use_secure_inclusion": "Use secure inclusion", + "secure_inclusion_warning": "Secure devices require additional bandwidth; too many secure devices can slow down your Z-Wave network. We recommend only using secure inclusion for devices that require it, like locks or garage door openers.", + "start_inclusion": "Start Inclusion", + "start_secure_inclusion": "Start Secure Inclusion", + "cancel_inclusion": "Cancel Inclusion", + "controller_in_inclusion_mode": "Your Z-Wave controller is now in inclusion mode.", + "follow_device_instructions": "Follow the directions that came with your device to trigger pairing on the device.", + "inclusion_failed": "The node could not be added. Please check the logs for more information.", + "inclusion_finished": "The node has been added. It may take a few minutes for all entities to show up as we finish setting up the node in the background.", + "view_device": "View Device" + }, + "remove_node": { + "title": "Remove a Z-Wave Node", + "introduction": "Remove a node from your Z-Wave network, and remove the associated device and entities from Home Assistant.", + "start_exclusion": "Start Exclusion", + "cancel_exclusion": "Cancel Exclusion", + "controller_in_exclusion_mode": "Your Z-Wave controller is now in exclusion mode.", + "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." } } }, @@ -3288,4 +3315,4 @@ } } } -} +} \ No newline at end of file