diff --git a/src/data/zha.ts b/src/data/zha.ts index 64abb74087..5913cf4073 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -55,6 +55,52 @@ export interface Cluster { type: string; } +export interface ClusterConfigurationData { + cluster_name: string; + cluster_id: number; + success: boolean; +} + +export interface ClusterAttributeData { + cluster_name: string; + cluster_id: number; + attributes: AttributeConfigurationStatus[]; +} + +export interface AttributeConfigurationStatus { + id: number; + name: string; + success: boolean | undefined; + min: number; + max: number; + change: number; +} + +export interface ClusterConfigurationStatus { + cluster: Cluster; + bindSuccess: boolean | undefined; + attributes: Map; +} + +interface ClusterConfigurationBindEvent { + type: "zha_channel_bind"; + zha_channel_msg_data: ClusterConfigurationData; +} + +interface ClusterConfigurationReportConfigurationEvent { + type: "zha_channel_configure_reporting"; + zha_channel_msg_data: ClusterAttributeData; +} + +interface ClusterConfigurationEventFinish { + type: "zha_channel_cfg_done"; +} + +export type ClusterConfigurationEvent = + | ClusterConfigurationReportConfigurationEvent + | ClusterConfigurationBindEvent + | ClusterConfigurationEventFinish; + export interface Command { name: string; id: number; @@ -89,10 +135,10 @@ export interface ZHAGroupMember { export const reconfigureNode = ( hass: HomeAssistant, ieeeAddress: string, - callbackFunction: any + callbackFunction: (message: ClusterConfigurationEvent) => void ) => { return hass.connection.subscribeMessage( - (message) => callbackFunction(message), + (message: ClusterConfigurationEvent) => callbackFunction(message), { type: "zha/devices/reconfigure", ieee: ieeeAddress, @@ -323,3 +369,7 @@ export const DEVICE_MESSAGE_TYPES = [ DEVICE_FULLY_INITIALIZED, ]; export const LOG_OUTPUT = "log_output"; +export const ZHA_CHANNEL_MSG = "zha_channel_message"; +export const ZHA_CHANNEL_MSG_BIND = "zha_channel_bind"; +export const ZHA_CHANNEL_MSG_CFG_RPT = "zha_channel_configure_reporting"; +export const ZHA_CHANNEL_CFG_DONE = "zha_channel_cfg_done"; diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts index 8e29d5af94..e32e984729 100644 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts +++ b/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts @@ -8,42 +8,62 @@ import { property, TemplateResult, } from "lit-element"; -import { createCloseHeading } from "../../../../../components/ha-dialog"; +import { mdiCheckCircle, mdiCloseCircle } from "@mdi/js"; +import "@material/mwc-button/mwc-button"; import { haStyleDialog } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; import { ZHAReconfigureDeviceDialogParams } from "./show-dialog-zha-reconfigure-device"; -import { IronAutogrowTextareaElement } from "@polymer/iron-autogrow-textarea"; -import "@polymer/paper-input/paper-textarea"; import "../../../../../components/ha-circular-progress"; -import { LOG_OUTPUT, reconfigureNode } from "../../../../../data/zha"; +import "../../../../../components/ha-svg-icon"; +import { + AttributeConfigurationStatus, + Cluster, + ClusterConfigurationEvent, + ClusterConfigurationStatus, + fetchClustersForZhaNode, + reconfigureNode, + ZHA_CHANNEL_CFG_DONE, + ZHA_CHANNEL_MSG_BIND, + ZHA_CHANNEL_MSG_CFG_RPT, +} from "../../../../../data/zha"; import { fireEvent } from "../../../../../common/dom/fire_event"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; @customElement("dialog-zha-reconfigure-device") class DialogZHAReconfigureDevice extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @internalProperty() private _active = false; + @internalProperty() private _status?: string; - @internalProperty() private _formattedEvents = ""; + @internalProperty() private _stages?: string[]; - @internalProperty() - private _params: ZHAReconfigureDeviceDialogParams | undefined = undefined; + @internalProperty() private _clusterConfigurationStatuses?: Map< + number, + ClusterConfigurationStatus + > = new Map(); - private _subscribed?: Promise<() => Promise>; + @internalProperty() private _params: + | ZHAReconfigureDeviceDialogParams + | undefined = undefined; - private _reconfigureDeviceTimeoutHandle: any = undefined; + @internalProperty() private _allSuccessful = true; - public async showDialog( - params: ZHAReconfigureDeviceDialogParams - ): Promise { + @internalProperty() private _showDetails = false; + + private _subscribed?: Promise; + + public showDialog(params: ZHAReconfigureDeviceDialogParams): void { this._params = params; - this._subscribe(params); + this._stages = undefined; } public closeDialog(): void { this._unsubscribe(); - this._formattedEvents = ""; this._params = undefined; + this._status = undefined; + this._stages = undefined; + this._clusterConfigurationStatuses = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -51,58 +71,311 @@ class DialogZHAReconfigureDevice extends LitElement { if (!this._params) { return html``; } + return html` -
- ${this._active - ? html` -

- ${this._params?.device.user_given_name || - this._params?.device.name} -

- - ` - : ""} -
- - + ${!this._status + ? html` +

+ ${this.hass.localize( + "ui.dialogs.zha_reconfigure_device.introduction" + )} +

+

+ + ${this.hass.localize( + "ui.dialogs.zha_reconfigure_device.battery_device_warning" + )} + +

+ + ${this.hass.localize( + "ui.dialogs.zha_reconfigure_device.start_reconfiguration" + )} + + ` + : ``} + ${this._status === "started" + ? html` +
+ +
+

+ + ${this.hass.localize( + "ui.dialogs.zha_reconfigure_device.in_progress" + )} + +

+

+ ${this.hass.localize( + "ui.dialogs.zha_reconfigure_device.run_in_background" + )} +

+
+
+ + ${this.hass.localize("ui.dialogs.generic.close")} + + + ${this._showDetails + ? this.hass.localize( + `ui.dialogs.zha_reconfigure_device.button_hide` + ) + : this.hass.localize( + `ui.dialogs.zha_reconfigure_device.button_show` + )} + + ` + : ``} + ${this._status === "failed" + ? html` +
+ +
+

+ ${this.hass.localize( + "ui.dialogs.zha_reconfigure_device.configuration_failed" + )} +

+
+
+ + ${this.hass.localize("ui.dialogs.generic.close")} + + + ${this._showDetails + ? this.hass.localize( + `ui.dialogs.zha_reconfigure_device.button_hide` + ) + : this.hass.localize( + `ui.dialogs.zha_reconfigure_device.button_show` + )} + + ` + : ``} + ${this._status === "finished" + ? html` +
+ +
+

+ ${this.hass.localize( + "ui.dialogs.zha_reconfigure_device.configuration_complete" + )} +

+
+
+ + ${this.hass.localize("ui.dialogs.generic.close")} + + + ${this._showDetails + ? this.hass.localize( + `ui.dialogs.zha_reconfigure_device.button_hide` + ) + : this.hass.localize( + `ui.dialogs.zha_reconfigure_device.button_show` + )} + + ` + : ``} + ${this._stages + ? html` +
+ ${this._stages.map( + (stage) => html` + + + ${stage} + + ` + )} +
+ ` + : ""} + ${this._showDetails + ? html` +
+

+ ${this.hass.localize( + `ui.dialogs.zha_reconfigure_device.cluster_header` + )} +

+

+ ${this.hass.localize( + `ui.dialogs.zha_reconfigure_device.bind_header` + )} +

+

+ ${this.hass.localize( + `ui.dialogs.zha_reconfigure_device.reporting_header` + )} +

+ + ${this._clusterConfigurationStatuses!.size > 0 + ? html` + ${Array.from( + this._clusterConfigurationStatuses!.values() + ).map( + (clusterStatus) => html` +
+ ${clusterStatus.cluster.name} +
+
+ ${clusterStatus.bindSuccess !== undefined + ? clusterStatus.bindSuccess + ? html` + + + + ` + : html` + + + + ` + : ""} +
+
+ ${clusterStatus.attributes.size > 0 + ? html` +
+
+ ${this.hass.localize( + `ui.dialogs.zha_reconfigure_device.attribute` + )} +
+
+
+ ${this.hass.localize( + `ui.dialogs.zha_reconfigure_device.min_max_change` + )} +
+
+ ${Array.from( + clusterStatus.attributes.values() + ).map( + (attribute) => html` + + ${attribute.name}: + ${attribute.success + ? html` + + + + ` + : html` + + + + `} + +
+ ${attribute.min}/${attribute.max}/${attribute.change} +
+ ` + )} +
+ ` + : ""} +
+ ` + )} + ` + : ""} +
+ ` + : ""}
`; } - private _handleMessage(message: any): void { - if (message.type === LOG_OUTPUT) { - this._formattedEvents += message.log_entry.message + "\n"; - const paperTextArea = this.shadowRoot!.querySelector("paper-textarea"); - if (paperTextArea) { - const textArea = (paperTextArea.inputElement as IronAutogrowTextareaElement) - .textarea; - textArea.scrollTop = textArea.scrollHeight; + private async _startReconfiguration(): Promise { + if (!this.hass || !this._params) { + return; + } + this._clusterConfigurationStatuses = new Map( + (await fetchClustersForZhaNode(this.hass, this._params.device.ieee)).map( + (cluster: Cluster) => [ + cluster.id, + { + cluster: cluster, + bindSuccess: undefined, + attributes: new Map(), + }, + ] + ) + ); + this._subscribe(this._params); + this._status = "started"; + } + + private _handleMessage(message: ClusterConfigurationEvent): void { + if (message.type === ZHA_CHANNEL_CFG_DONE) { + this._unsubscribe(); + this._status = this._allSuccessful ? "finished" : "failed"; + } else { + const clusterConfigurationStatus = this._clusterConfigurationStatuses!.get( + message.zha_channel_msg_data.cluster_id + ); + if (message.type === ZHA_CHANNEL_MSG_BIND) { + if (!this._stages) { + this._stages = ["binding"]; + } + const success = message.zha_channel_msg_data.success; + clusterConfigurationStatus!.bindSuccess = success; + this._allSuccessful = this._allSuccessful && success; } + if (message.type === ZHA_CHANNEL_MSG_CFG_RPT) { + if (this._stages && !this._stages.includes("reporting")) { + this._stages.push("reporting"); + } + const attributes = message.zha_channel_msg_data.attributes; + Object.keys(attributes).forEach((name) => { + const attribute = attributes[name]; + clusterConfigurationStatus!.attributes.set(attribute.id, attribute); + this._allSuccessful = this._allSuccessful && attribute.success; + }); + } + this.requestUpdate(); } } private _unsubscribe(): void { - this._active = false; - if (this._reconfigureDeviceTimeoutHandle) { - clearTimeout(this._reconfigureDeviceTimeoutHandle); - } if (this._subscribed) { this._subscribed.then((unsub) => unsub()); this._subscribed = undefined; @@ -113,33 +386,66 @@ class DialogZHAReconfigureDevice extends LitElement { if (!this.hass) { return; } - this._active = true; this._subscribed = reconfigureNode( this.hass, params.device.ieee, this._handleMessage.bind(this) ); - this._reconfigureDeviceTimeoutHandle = setTimeout( - () => this._unsubscribe(), - 60000 - ); + } + + private _toggleDetails() { + this._showDetails = !this._showDetails; } static get styles(): CSSResult[] { return [ haStyleDialog, css` - ha-circular-progress { - padding: 20px; + .wrapper { + display: grid; + grid-template-columns: 3fr 1fr 2fr; } - .searching { - margin-top: 20px; + .attributes { + display: grid; + grid-template-columns: 1fr 1fr; + } + .grid-item { + border: 1px solid; + padding: 7px; + } + .success { + color: var(--success-color); + } + + .failed { + color: var(--warning-color); + } + + .flex-container { display: flex; - flex-direction: column; align-items: center; } - .log { - padding: 16px; + + .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; } `, ]; diff --git a/src/translations/en.json b/src/translations/en.json index 62cda1eafd..0f1cb8de63 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -760,7 +760,22 @@ "update": "Update" }, "zha_reconfigure_device": { - "heading": "Reconfiguring device" + "heading": "Reconfiguring device", + "configuring_alt": "Configuring", + "introduction": "Reconfigure a device on your Zigbee network. Use this feature if your device is not functioning correctly.", + "battery_device_warning": "You will need to wake battery powered devices before starting the reconfiguration process. Refer to your device's manual for instructions on how to wake the device.", + "run_in_background": "You can close this dialog and the reconfiguration will continue in the background.", + "start_reconfiguration": "Start Reconfiguration", + "in_progress": "The device is being reconfigured. This may take some time.", + "configuration_failed": "The device reconfiguration failed. Additional information may be available in the logs.", + "configuration_complete": "Device reconfiguration complete.", + "button_show": "Show Details", + "button_hide": "Hide Details", + "cluster_header": "Cluster", + "bind_header": "Binding", + "reporting_header": "Reporting", + "attribute": "Attribute", + "min_max_change": "min/max/change" }, "zha_device_info": { "manuf": "by {manufacturer}",