diff --git a/src/panels/config/devices/device-detail/integration-elements/zwave_js/device-actions.ts b/src/panels/config/devices/device-detail/integration-elements/zwave_js/device-actions.ts index ffe1a23305..96c36c045d 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zwave_js/device-actions.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zwave_js/device-actions.ts @@ -1,6 +1,7 @@ import { mdiChatQuestion, mdiCog, + mdiDelete, mdiDeleteForever, mdiHospitalBox, mdiInformation, @@ -16,17 +17,19 @@ import { fetchZwaveIsNodeFirmwareUpdateInProgress, fetchZwaveNetworkStatus, fetchZwaveNodeStatus, + fetchZwaveProvisioningEntries, + unprovisionZwaveSmartStartNode, } from "../../../../../../data/zwave_js"; import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../../../../types"; import { showZWaveJSRebuildNodeRoutesDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-rebuild-node-routes"; import { showZWaveJSNodeStatisticsDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-node-statistics"; import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node"; -import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node"; import { showZWaveJSUpdateFirmwareNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-update-firmware-node"; import type { DeviceAction } from "../../../ha-config-device-page"; import { showZWaveJSHardResetControllerDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-hard-reset-controller"; import { showZWaveJSAddNodeDialog } from "../../../../integrations/integration-panels/zwave_js/add-node/show-dialog-zwave_js-add-node"; +import { showZWaveJSRemoveNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-node"; export const getZwaveDeviceActions = async ( el: HTMLElement, @@ -47,6 +50,43 @@ export const getZwaveDeviceActions = async ( const entryId = configEntry.entry_id; + const provisioningEntries = await fetchZwaveProvisioningEntries( + hass, + entryId + ); + const provisioningEntry = provisioningEntries.find( + (entry) => entry.device_id === device.id + ); + if (provisioningEntry && !provisioningEntry.nodeId) { + return [ + { + label: hass.localize("ui.panel.config.devices.delete_device"), + classes: "warning", + icon: mdiDelete, + action: async () => { + const confirm = await showConfirmationDialog(el, { + title: hass.localize( + "ui.panel.config.zwave_js.provisioned.confirm_unprovision_title" + ), + text: hass.localize( + "ui.panel.config.zwave_js.provisioned.confirm_unprovision_text", + { name: device.name_by_user || device.name } + ), + confirmText: hass.localize("ui.common.remove"), + destructive: true, + }); + + if (confirm) { + await unprovisionZwaveSmartStartNode( + hass, + entryId, + provisioningEntry.dsk + ); + } + }, + }, + ]; + } const nodeStatus = await fetchZwaveNodeStatus(hass, device.id); if (!nodeStatus) { @@ -84,16 +124,6 @@ export const getZwaveDeviceActions = async ( device, }), }, - { - label: hass.localize( - "ui.panel.config.zwave_js.device_info.remove_failed" - ), - icon: mdiDeleteForever, - action: () => - showZWaveJSRemoveFailedNodeDialog(el, { - device_id: device.id, - }), - }, { label: hass.localize( "ui.panel.config.zwave_js.device_info.node_statistics" @@ -103,6 +133,16 @@ export const getZwaveDeviceActions = async ( showZWaveJSNodeStatisticsDialog(el, { device, }), + }, + { + label: hass.localize("ui.panel.config.devices.delete_device"), + classes: "warning", + icon: mdiDelete, + action: () => + showZWaveJSRemoveNodeDialog(el, { + deviceId: device.id, + entryId, + }), } ); } diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-failed-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-failed-node.ts deleted file mode 100644 index e5b46004a1..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-failed-node.ts +++ /dev/null @@ -1,234 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { mdiCheckCircle, mdiCloseCircle, mdiRobotDead } from "@mdi/js"; -import type { UnsubscribeFunc } from "home-assistant-js-websocket"; -import type { CSSResultGroup } from "lit"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { fireEvent } from "../../../../../common/dom/fire_event"; -import "../../../../../components/ha-spinner"; -import { createCloseHeading } from "../../../../../components/ha-dialog"; -import type { ZWaveJSRemovedNode } from "../../../../../data/zwave_js"; -import { removeFailedZwaveNode } from "../../../../../data/zwave_js"; -import { haStyleDialog } from "../../../../../resources/styles"; -import type { HomeAssistant } from "../../../../../types"; -import type { ZWaveJSRemoveFailedNodeDialogParams } from "./show-dialog-zwave_js-remove-failed-node"; - -@customElement("dialog-zwave_js-remove-failed-node") -class DialogZWaveJSRemoveFailedNode extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private device_id?: string; - - @state() private _status = ""; - - @state() private _error?: any; - - @state() private _node?: ZWaveJSRemovedNode; - - private _subscribed?: Promise; - - public disconnectedCallback(): void { - super.disconnectedCallback(); - this._unsubscribe(); - } - - public async showDialog( - params: ZWaveJSRemoveFailedNodeDialogParams - ): Promise { - this.device_id = params.device_id; - } - - public closeDialog(): void { - this._unsubscribe(); - this.device_id = undefined; - this._status = ""; - - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - - public closeDialogFinished(): void { - history.back(); - this.closeDialog(); - } - - protected render() { - if (!this.device_id) { - return nothing; - } - - return html` - - ${this._status === "" - ? html` -
- -
- ${this.hass.localize( - "ui.panel.config.zwave_js.remove_failed_node.introduction" - )} -
-
- - ${this.hass.localize( - "ui.panel.config.zwave_js.remove_failed_node.remove_device" - )} - - ` - : ``} - ${this._status === "started" - ? html` -
- -
-

- - ${this.hass.localize( - "ui.panel.config.zwave_js.remove_failed_node.in_progress" - )} - -

-
-
- ` - : ``} - ${this._status === "failed" - ? html` -
- -
-

- ${this.hass.localize( - "ui.panel.config.zwave_js.remove_failed_node.removal_failed" - )} -

- ${this._error - ? html`

${this._error.message}

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

- ${this.hass.localize( - "ui.panel.config.zwave_js.remove_failed_node.removal_finished", - { id: this._node!.node_id } - )} -

-
-
- - ${this.hass.localize("ui.common.close")} - - ` - : ``} -
- `; - } - - private _startExclusion(): void { - if (!this.hass) { - return; - } - this._status = "started"; - this._subscribed = removeFailedZwaveNode( - this.hass, - this.device_id!, - (message: any) => this._handleMessage(message) - ).catch((error) => { - this._status = "failed"; - this._error = error; - return undefined; - }); - } - - private _handleMessage(message: any): void { - if (message.event === "exclusion started") { - this._status = "started"; - } - if (message.event === "node removed") { - this._status = "finished"; - this._node = message.node; - this._unsubscribe(); - } - } - - private async _unsubscribe(): Promise { - if (this._subscribed) { - const unsubFunc = await this._subscribed; - if (unsubFunc instanceof Function) { - unsubFunc(); - } - this._subscribed = undefined; - } - if (this._status !== "finished") { - this._status = ""; - } - } - - static get styles(): CSSResultGroup { - return [ - haStyleDialog, - css` - .success { - color: var(--success-color); - } - - .failed { - color: var(--warning-color); - } - - .flex-container { - display: flex; - align-items: center; - } - - ha-svg-icon { - width: 68px; - height: 48px; - } - - .flex-container ha-spinner, - .flex-container ha-svg-icon { - margin-right: 20px; - margin-inline-end: 20px; - margin-inline-start: initial; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-zwave_js-remove-failed-node": DialogZWaveJSRemoveFailedNode; - } -} 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 index 528e35740d..58e88b3c00 100644 --- 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 @@ -2,6 +2,7 @@ import { mdiCheckCircle, mdiClose, mdiCloseCircle, + mdiRobotDead, mdiVectorSquareRemove, } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; @@ -17,6 +18,14 @@ import "../../../../../components/ha-spinner"; import { haStyleDialog } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; import type { ZWaveJSRemoveNodeDialogParams } from "./show-dialog-zwave_js-remove-node"; +import { + fetchZwaveNodeStatus, + NodeStatus, + removeFailedZwaveNode, +} from "../../../../../data/zwave_js"; +import "../../../../../components/ha-list-item"; +import "../../../../../components/ha-icon-next"; +import type { DeviceRegistryEntry } from "../../../../../data/device_registry"; const EXCLUSION_TIMEOUT_SECONDS = 120; @@ -30,10 +39,16 @@ export interface ZWaveJSRemovedNode { class DialogZWaveJSRemoveNode extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private entry_id?: string; + @state() private _entryId?: string; + + @state() private _deviceId?: string; + + private _device?: DeviceRegistryEntry; @state() private _step: | "start" + | "start_exclusion" + | "start_removal" | "exclusion" | "remove" | "finished" @@ -42,7 +57,7 @@ class DialogZWaveJSRemoveNode extends LitElement { @state() private _node?: ZWaveJSRemovedNode; - @state() private _removedCallback?: () => void; + @state() private _onClose?: () => void; private _removeNodeTimeoutHandle?: number; @@ -58,15 +73,23 @@ class DialogZWaveJSRemoveNode extends LitElement { public async showDialog( params: ZWaveJSRemoveNodeDialogParams ): Promise { - this.entry_id = params.entry_id; - this._removedCallback = params.removedCallback; - if (params.skipConfirmation) { + this._entryId = params.entryId; + this._deviceId = params.deviceId; + this._onClose = params.onClose; + if (this._deviceId) { + const nodeStatus = await fetchZwaveNodeStatus(this.hass, this._deviceId!); + this._device = this.hass.devices[this._deviceId]; + this._step = + nodeStatus.status === NodeStatus.Dead ? "start_removal" : "start"; + } else if (params.skipConfirmation) { this._startExclusion(); + } else { + this._step = "start_exclusion"; } } protected render() { - if (!this.entry_id) { + if (!this._entryId) { return nothing; } @@ -75,7 +98,12 @@ class DialogZWaveJSRemoveNode extends LitElement { ); return html` - + + + `; + } + + if (this._step === "start_removal") { + return html` + +

+ ${this.hass.localize( + "ui.panel.config.zwave_js.remove_node.failed_node_intro", + { name: this._device!.name_by_user || this._device!.name } + )} +

+ `; + } + + if (this._step === "start_exclusion") { + return html` + +

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

`; } @@ -143,30 +212,59 @@ class DialogZWaveJSRemoveNode extends LitElement { `; } - private _renderAction(): TemplateResult { + private _renderAction() { + if (this._step === "start") { + return nothing; + } + + if (this._step === "start_removal") { + return html` + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize("ui.common.remove")} + + `; + } + + if (this._step === "start_exclusion") { + return html` + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize( + "ui.panel.config.zwave_js.remove_node.start_exclusion" + )} + + `; + } + return html` - + ${this.hass.localize( - this._step === "start" - ? "ui.panel.config.zwave_js.remove_node.start_exclusion" - : this._step === "exclusion" - ? "ui.panel.config.zwave_js.remove_node.cancel_exclusion" - : "ui.common.close" + this._step === "exclusion" + ? "ui.panel.config.zwave_js.remove_node.cancel_exclusion" + : "ui.common.close" )} `; } - private _startExclusion(): void { + private _startExclusion() { this._subscribed = this.hass.connection - .subscribeMessage((message) => this._handleMessage(message), { + .subscribeMessage(this._handleMessage, { type: "zwave_js/remove_node", - entry_id: this.entry_id, + entry_id: this._entryId, }) .catch((err) => { this._step = "failed"; @@ -180,7 +278,20 @@ class DialogZWaveJSRemoveNode extends LitElement { }, EXCLUSION_TIMEOUT_SECONDS * 1000); } - private _handleMessage(message: any): void { + private _startRemoval() { + this._subscribed = removeFailedZwaveNode( + this.hass, + this._deviceId!, + this._handleMessage + ).catch((err) => { + this._step = "failed"; + this._error = err.message; + return undefined; + }); + this._step = "remove"; + } + + private _handleMessage = (message: any) => { if (message.event === "exclusion failed") { this._unsubscribe(); this._step = "failed"; @@ -192,17 +303,14 @@ class DialogZWaveJSRemoveNode extends LitElement { this._step = "finished"; this._node = message.node; this._unsubscribe(); - if (this._removedCallback) { - this._removedCallback(); - } } - } + }; private _stopExclusion(): void { try { this.hass.callWS({ type: "zwave_js/stop_exclusion", - entry_id: this.entry_id, + entry_id: this._entryId, }); } catch (err) { // eslint-disable-next-line no-console @@ -224,10 +332,16 @@ class DialogZWaveJSRemoveNode extends LitElement { }; public closeDialog(): void { - this._unsubscribe(); - this.entry_id = undefined; - this._step = "start"; + this._entryId = undefined; + } + public handleDialogClosed(): void { + this._unsubscribe(); + this._entryId = undefined; + this._step = "start"; + if (this._onClose) { + this._onClose(); + } fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -266,6 +380,14 @@ class DialogZWaveJSRemoveNode extends LitElement { ha-alert { width: 100%; } + + .menu-options { + align-self: stretch; + } + + ha-list-item { + --mdc-list-side-padding: 24px; + } `, ]; } diff --git a/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node.ts deleted file mode 100644 index ae902f28ae..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { fireEvent } from "../../../../../common/dom/fire_event"; - -export interface ZWaveJSRemoveFailedNodeDialogParams { - device_id: string; -} - -export const loadRemoveFailedNodeDialog = () => - import("./dialog-zwave_js-remove-failed-node"); - -export const showZWaveJSRemoveFailedNodeDialog = ( - element: HTMLElement, - removeFailedNodeDialogParams: ZWaveJSRemoveFailedNodeDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-zwave_js-remove-failed-node", - dialogImport: loadRemoveFailedNodeDialog, - dialogParams: removeFailedNodeDialogParams, - }); -}; 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 index ac5fe5b063..e501a92140 100644 --- 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 @@ -1,9 +1,10 @@ import { fireEvent } from "../../../../../common/dom/fire_event"; export interface ZWaveJSRemoveNodeDialogParams { - entry_id: string; + entryId: string; + deviceId?: string; skipConfirmation?: boolean; - removedCallback?: () => void; + onClose?: () => void; } export const loadRemoveNodeDialog = () => 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 f82c38de9c..e2505789ee 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 @@ -604,7 +604,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) { history.back(); } - private async _fetchData() { + private _fetchData = async () => { if (!this.configEntryId) { return; } @@ -638,7 +638,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) { this._dataCollectionOptIn = dataCollectionStatus.opted_in === true || dataCollectionStatus.enabled === true; - } + }; private async _addNodeClicked() { this._openInclusionDialog(); @@ -646,10 +646,10 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) { private async _removeNodeClicked() { showZWaveJSRemoveNodeDialog(this, { - entry_id: this.configEntryId!, + entryId: this.configEntryId!, skipConfirmation: this._network?.controller.inclusion_state === InclusionState.Excluding, - removedCallback: () => this._fetchData(), + onClose: this._fetchData, }); } diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-provisioned.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-provisioned.ts index 38a26852bd..e4b13608d6 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-provisioned.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-provisioned.ts @@ -123,18 +123,21 @@ class ZWaveJSProvisioned extends LitElement { } private _unprovision = async (ev) => { - const dsk = ev.currentTarget.provisioningEntry.dsk; + const { dsk, nodeId } = ev.currentTarget.provisioningEntry; const confirm = await showConfirmationDialog(this, { title: this.hass.localize( "ui.panel.config.zwave_js.provisioned.confirm_unprovision_title" ), text: this.hass.localize( - "ui.panel.config.zwave_js.provisioned.confirm_unprovision_text" + nodeId + ? "ui.panel.config.zwave_js.provisioned.confirm_unprovision_text_included" + : "ui.panel.config.zwave_js.provisioned.confirm_unprovision_text" ), confirmText: this.hass.localize( - "ui.panel.config.zwave_js.provisioned.unprovison" + "ui.panel.config.zwave_js.provisioned.unprovision" ), + destructive: true, }); if (!confirm) { diff --git a/src/translations/en.json b/src/translations/en.json index 964324323c..18cdf3d5c4 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5880,7 +5880,6 @@ "installer_settings": "Installer settings", "reinterview_device": "Re-interview", "rebuild_routes": "Rebuild routes", - "remove_failed": "Remove failed", "update_firmware": "Update", "highest_security": "Highest security", "hard_reset_controller": "Factory reset", @@ -6126,8 +6125,8 @@ "unprovision": "Unprovision", "included": "Included", "not_included": "Not Included", - "confirm_unprovision_title": "Are you sure you want to unprovision the device?", - "confirm_unprovision_text": "If you unprovision the device it will not be added to Home Assistant when it is powered on. If it is already added to Home Assistant, removing the provisioned device will not remove it from Home Assistant." + "confirm_unprovision_title": "Remove device?", + "confirm_unprovision_text": "{name} will be permanently removed from Home Assistant and your Z-Wave network." }, "security_classes": { "None": { @@ -6152,22 +6151,18 @@ }, "remove_node": { "title": "Remove a Z-Wave device", - "introduction": "Remove a device from your Z-Wave network, and remove the associated device and entities from Home Assistant.", + "introduction": "There are two ways to remove a device from your Z-Wave network depending on the state of the device. If the device is working, you can remove it by following the instructions that came with your device to put it into exclusion mode. If the device is unavailable, you can force-remove it from the network.", + "exclusion_intro": "Remove a device from your Z-Wave network, and remove the associated device and entities from Home Assistant.", + "failed_node_intro": "{name} is unable to connect to your Z-wave Network. Home Assistant can force-remove a device for you. After removing it you might need to factory reset your device.", + "menu_exclude_device": "Remove a working device", + "menu_remove_device": "Force-remove an unavailable device", "start_exclusion": "Start exclusion", "cancel_exclusion": "Cancel exclusion", "follow_device_instructions": "Follow the directions that came with your device to trigger exclusion on the device.", "removing_device": "Removing device", - "exclusion_failed": "An error occurred during exclusion. Please check the logs for more information.", + "exclusion_failed": "An error occurred. Please check the logs for more information.", "exclusion_finished": "Device {id} has been removed from your Z-Wave network." }, - "remove_failed_node": { - "title": "Remove a failed Z-Wave device", - "introduction": "Remove a failed device from your Z-Wave network. Use this if you are unable to exclude a device normally because it is broken.", - "remove_device": "Remove device", - "in_progress": "The device removal is in progress.", - "removal_finished": "Device {id} has been removed from your Z-Wave network.", - "removal_failed": "The device could not be 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.",