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;
}
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 {
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";

View File

@ -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<void>>;
@internalProperty() private _params:
| ZHAReconfigureDeviceDialogParams
| undefined = undefined;
private _reconfigureDeviceTimeoutHandle: any = undefined;
@internalProperty() private _allSuccessful = true;
public async showDialog(
params: ZHAReconfigureDeviceDialogParams
): Promise<void> {
@internalProperty() private _showDetails = false;
private _subscribed?: Promise<UnsubscribeFunc>;
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`
<ha-dialog
open
hideActions
@closing="${this.closeDialog}"
@closed="${this.closeDialog}"
.heading=${createCloseHeading(
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._active
? html`
<h1>
${this._params?.device.user_given_name ||
this._params?.device.name}
</h1>
<ha-circular-progress
active
alt="Searching"
></ha-circular-progress>
`
: ""}
</div>
<paper-textarea
readonly
max-rows="10"
class="log"
value="${this._formattedEvents}"
>
</paper-textarea>
${!this._status
? html`
<p>
${this.hass.localize(
"ui.dialogs.zha_reconfigure_device.introduction"
)}
</p>
<p>
<em>
${this.hass.localize(
"ui.dialogs.zha_reconfigure_device.battery_device_warning"
)}
</em>
</p>
<mwc-button
slot="primaryAction"
@click=${this._startReconfiguration}
>
${this.hass.localize(
"ui.dialogs.zha_reconfigure_device.start_reconfiguration"
)}
</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>
`;
}
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<void> {
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<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 {
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;
}
`,
];

View File

@ -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}",