Reconfigure ZHA device take 2 (#8990)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
David F. Mulcahey 2021-04-28 06:30:09 -04:00 committed by GitHub
parent 7304544c37
commit 9690434cac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 437 additions and 66 deletions

View File

@ -55,6 +55,52 @@ export interface Cluster {
type: string; 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<number, AttributeConfigurationStatus>;
}
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 { export interface Command {
name: string; name: string;
id: number; id: number;
@ -89,10 +135,10 @@ export interface ZHAGroupMember {
export const reconfigureNode = ( export const reconfigureNode = (
hass: HomeAssistant, hass: HomeAssistant,
ieeeAddress: string, ieeeAddress: string,
callbackFunction: any callbackFunction: (message: ClusterConfigurationEvent) => void
) => { ) => {
return hass.connection.subscribeMessage( return hass.connection.subscribeMessage(
(message) => callbackFunction(message), (message: ClusterConfigurationEvent) => callbackFunction(message),
{ {
type: "zha/devices/reconfigure", type: "zha/devices/reconfigure",
ieee: ieeeAddress, ieee: ieeeAddress,
@ -323,3 +369,7 @@ export const DEVICE_MESSAGE_TYPES = [
DEVICE_FULLY_INITIALIZED, DEVICE_FULLY_INITIALIZED,
]; ];
export const LOG_OUTPUT = "log_output"; 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";

View File

@ -8,42 +8,62 @@ import {
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } 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 { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { ZHAReconfigureDeviceDialogParams } from "./show-dialog-zha-reconfigure-device"; 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 "../../../../../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 { fireEvent } from "../../../../../common/dom/fire_event";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { createCloseHeading } from "../../../../../components/ha-dialog";
@customElement("dialog-zha-reconfigure-device") @customElement("dialog-zha-reconfigure-device")
class DialogZHAReconfigureDevice extends LitElement { class DialogZHAReconfigureDevice extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _active = false; @internalProperty() private _status?: string;
@internalProperty() private _formattedEvents = ""; @internalProperty() private _stages?: string[];
@internalProperty() @internalProperty() private _clusterConfigurationStatuses?: Map<
private _params: ZHAReconfigureDeviceDialogParams | undefined = undefined; number,
ClusterConfigurationStatus
> = new Map();
private _subscribed?: Promise<() => Promise<void>>; @internalProperty() private _params:
| ZHAReconfigureDeviceDialogParams
| undefined = undefined;
private _reconfigureDeviceTimeoutHandle: any = undefined; @internalProperty() private _allSuccessful = true;
public async showDialog( @internalProperty() private _showDetails = false;
params: ZHAReconfigureDeviceDialogParams
): Promise<void> { private _subscribed?: Promise<UnsubscribeFunc>;
public showDialog(params: ZHAReconfigureDeviceDialogParams): void {
this._params = params; this._params = params;
this._subscribe(params); this._stages = undefined;
} }
public closeDialog(): void { public closeDialog(): void {
this._unsubscribe(); this._unsubscribe();
this._formattedEvents = "";
this._params = undefined; this._params = undefined;
this._status = undefined;
this._stages = undefined;
this._clusterConfigurationStatuses = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@ -51,58 +71,311 @@ class DialogZHAReconfigureDevice extends LitElement {
if (!this._params) { if (!this._params) {
return html``; return html``;
} }
return html` return html`
<ha-dialog <ha-dialog
open open
hideActions @closed="${this.closeDialog}"
@closing="${this.closeDialog}"
.heading=${createCloseHeading( .heading=${createCloseHeading(
this.hass, this.hass,
this.hass.localize(`ui.dialogs.zha_reconfigure_device.heading`) this.hass.localize(`ui.dialogs.zha_reconfigure_device.heading`) +
": " +
(this._params?.device.user_given_name || this._params?.device.name)
)} )}
> >
<div class="searching"> ${!this._status
${this._active ? html`
? html` <p>
<h1> ${this.hass.localize(
${this._params?.device.user_given_name || "ui.dialogs.zha_reconfigure_device.introduction"
this._params?.device.name} )}
</h1> </p>
<ha-circular-progress <p>
active <em>
alt="Searching" ${this.hass.localize(
></ha-circular-progress> "ui.dialogs.zha_reconfigure_device.battery_device_warning"
` )}
: ""} </em>
</div> </p>
<paper-textarea <mwc-button
readonly slot="primaryAction"
max-rows="10" @click=${this._startReconfiguration}
class="log" >
value="${this._formattedEvents}" ${this.hass.localize(
> "ui.dialogs.zha_reconfigure_device.start_reconfiguration"
</paper-textarea> )}
</mwc-button>
`
: ``}
${this._status === "started"
? html`
<div class="flex-container">
<ha-circular-progress active></ha-circular-progress>
<div class="status">
<p>
<b>
${this.hass.localize(
"ui.dialogs.zha_reconfigure_device.in_progress"
)}
</b>
</p>
<p>
${this.hass.localize(
"ui.dialogs.zha_reconfigure_device.run_in_background"
)}
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.dialogs.generic.close")}
</mwc-button>
<mwc-button slot="secondaryAction" @click=${this._toggleDetails}>
${this._showDetails
? this.hass.localize(
`ui.dialogs.zha_reconfigure_device.button_hide`
)
: this.hass.localize(
`ui.dialogs.zha_reconfigure_device.button_show`
)}
</mwc-button>
`
: ``}
${this._status === "failed"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCloseCircle}
class="failed"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.dialogs.zha_reconfigure_device.configuration_failed"
)}
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.dialogs.generic.close")}
</mwc-button>
<mwc-button slot="secondaryAction" @click=${this._toggleDetails}>
${this._showDetails
? this.hass.localize(
`ui.dialogs.zha_reconfigure_device.button_hide`
)
: this.hass.localize(
`ui.dialogs.zha_reconfigure_device.button_show`
)}
</mwc-button>
`
: ``}
${this._status === "finished"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.dialogs.zha_reconfigure_device.configuration_complete"
)}
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.dialogs.generic.close")}
</mwc-button>
<mwc-button slot="secondaryAction" @click=${this._toggleDetails}>
${this._showDetails
? this.hass.localize(
`ui.dialogs.zha_reconfigure_device.button_hide`
)
: this.hass.localize(
`ui.dialogs.zha_reconfigure_device.button_show`
)}
</mwc-button>
`
: ``}
${this._stages
? html`
<div class="stages">
${this._stages.map(
(stage) => html`
<span class="stage">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
${stage}
</span>
`
)}
</div>
`
: ""}
${this._showDetails
? html`
<div class="wrapper">
<h2 class="grid-item">
${this.hass.localize(
`ui.dialogs.zha_reconfigure_device.cluster_header`
)}
</h2>
<h2 class="grid-item">
${this.hass.localize(
`ui.dialogs.zha_reconfigure_device.bind_header`
)}
</h2>
<h2 class="grid-item">
${this.hass.localize(
`ui.dialogs.zha_reconfigure_device.reporting_header`
)}
</h2>
${this._clusterConfigurationStatuses!.size > 0
? html`
${Array.from(
this._clusterConfigurationStatuses!.values()
).map(
(clusterStatus) => html`
<div class="grid-item">
${clusterStatus.cluster.name}
</div>
<div class="grid-item">
${clusterStatus.bindSuccess !== undefined
? clusterStatus.bindSuccess
? html`
<span class="stage">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
</span>
`
: html`
<span class="stage">
<ha-svg-icon
.path=${mdiCloseCircle}
class="failed"
></ha-svg-icon>
</span>
`
: ""}
</div>
<div class="grid-item">
${clusterStatus.attributes.size > 0
? html`
<div class="attributes">
<div class="grid-item">
${this.hass.localize(
`ui.dialogs.zha_reconfigure_device.attribute`
)}
</div>
<div class="grid-item">
<div>
${this.hass.localize(
`ui.dialogs.zha_reconfigure_device.min_max_change`
)}
</div>
</div>
${Array.from(
clusterStatus.attributes.values()
).map(
(attribute) => html`
<span class="grid-item">
${attribute.name}:
${attribute.success
? html`
<span class="stage">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
</span>
`
: html`
<span class="stage">
<ha-svg-icon
.path=${mdiCloseCircle}
class="failed"
></ha-svg-icon>
</span>
`}
</span>
<div class="grid-item">
${attribute.min}/${attribute.max}/${attribute.change}
</div>
`
)}
</div>
`
: ""}
</div>
`
)}
`
: ""}
</div>
`
: ""}
</ha-dialog> </ha-dialog>
`; `;
} }
private _handleMessage(message: any): void { private async _startReconfiguration(): Promise<void> {
if (message.type === LOG_OUTPUT) { if (!this.hass || !this._params) {
this._formattedEvents += message.log_entry.message + "\n"; return;
const paperTextArea = this.shadowRoot!.querySelector("paper-textarea"); }
if (paperTextArea) { this._clusterConfigurationStatuses = new Map(
const textArea = (paperTextArea.inputElement as IronAutogrowTextareaElement) (await fetchClustersForZhaNode(this.hass, this._params.device.ieee)).map(
.textarea; (cluster: Cluster) => [
textArea.scrollTop = textArea.scrollHeight; cluster.id,
{
cluster: cluster,
bindSuccess: undefined,
attributes: new Map<number, AttributeConfigurationStatus>(),
},
]
)
);
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 { private _unsubscribe(): void {
this._active = false;
if (this._reconfigureDeviceTimeoutHandle) {
clearTimeout(this._reconfigureDeviceTimeoutHandle);
}
if (this._subscribed) { if (this._subscribed) {
this._subscribed.then((unsub) => unsub()); this._subscribed.then((unsub) => unsub());
this._subscribed = undefined; this._subscribed = undefined;
@ -113,33 +386,66 @@ class DialogZHAReconfigureDevice extends LitElement {
if (!this.hass) { if (!this.hass) {
return; return;
} }
this._active = true;
this._subscribed = reconfigureNode( this._subscribed = reconfigureNode(
this.hass, this.hass,
params.device.ieee, params.device.ieee,
this._handleMessage.bind(this) this._handleMessage.bind(this)
); );
this._reconfigureDeviceTimeoutHandle = setTimeout( }
() => this._unsubscribe(),
60000 private _toggleDetails() {
); this._showDetails = !this._showDetails;
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
haStyleDialog, haStyleDialog,
css` css`
ha-circular-progress { .wrapper {
padding: 20px; display: grid;
grid-template-columns: 3fr 1fr 2fr;
} }
.searching { .attributes {
margin-top: 20px; 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; display: flex;
flex-direction: column;
align-items: center; 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;
} }
`, `,
]; ];

View File

@ -760,7 +760,22 @@
"update": "Update" "update": "Update"
}, },
"zha_reconfigure_device": { "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": { "zha_device_info": {
"manuf": "by {manufacturer}", "manuf": "by {manufacturer}",