From d4be171df942b055c979d356aea8a84505906bed Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 7 Mar 2019 13:53:06 -0500 Subject: [PATCH] Direct device binding for ZHA config panel (#2856) * device binding * review comments * Update zha-binding.ts --- src/data/zha.ts | 37 ++++- src/panels/config/zha/ha-config-zha.ts | 63 +++++--- src/panels/config/zha/types.ts | 6 +- src/panels/config/zha/zha-binding.ts | 211 +++++++++++++++++++++++++ 4 files changed, 287 insertions(+), 30 deletions(-) create mode 100644 src/panels/config/zha/zha-binding.ts diff --git a/src/data/zha.ts b/src/data/zha.ts index 8c50b82f3c..30b9cfbfa4 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -1,12 +1,6 @@ import { HassEntity } from "home-assistant-js-websocket"; import { HomeAssistant } from "../types"; -export interface ZHADeviceEntity extends HassEntity { - device_info?: { - identifiers: any[]; - }; -} - export interface ZHAEntityReference extends HassEntity { name: string; } @@ -78,6 +72,37 @@ export const fetchDevices = (hass: HomeAssistant): Promise => type: "zha/devices", }); +export const fetchBindableDevices = ( + hass: HomeAssistant, + ieeeAddress: string +): Promise => + hass.callWS({ + type: "zha/devices/bindable", + ieee: ieeeAddress, + }); + +export const bindDevices = ( + hass: HomeAssistant, + sourceIEEE: string, + targetIEEE: string +): Promise => + hass.callWS({ + type: "zha/devices/bind", + source_ieee: sourceIEEE, + target_ieee: targetIEEE, + }); + +export const unbindDevices = ( + hass: HomeAssistant, + sourceIEEE: string, + targetIEEE: string +): Promise => + hass.callWS({ + type: "zha/devices/unbind", + source_ieee: sourceIEEE, + target_ieee: targetIEEE, + }); + export const readAttributeValue = ( hass: HomeAssistant, data: ReadAttributeServiceData diff --git a/src/panels/config/zha/ha-config-zha.ts b/src/panels/config/zha/ha-config-zha.ts index ef9c06b59e..1708b9f5a8 100755 --- a/src/panels/config/zha/ha-config-zha.ts +++ b/src/panels/config/zha/ha-config-zha.ts @@ -3,37 +3,37 @@ import "@polymer/app-layout/app-toolbar/app-toolbar"; import { html, LitElement, - PropertyDeclarations, + property, + PropertyValues, TemplateResult, CSSResult, } from "lit-element"; import "@polymer/paper-icon-button/paper-icon-button"; -import { HassEntity } from "home-assistant-js-websocket"; import { HASSDomEvent } from "../../../common/dom/fire_event"; -import { Cluster } from "../../../data/zha"; +import { Cluster, ZHADevice, fetchBindableDevices } from "../../../data/zha"; import "../../../layouts/ha-app-layout"; import "../../../components/ha-paper-icon-button-arrow-prev"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; -import { ZHAClusterSelectedParams, ZHANodeSelectedParams } from "./types"; +import { ZHAClusterSelectedParams, ZHADeviceSelectedParams } from "./types"; import "./zha-cluster-attributes"; import "./zha-cluster-commands"; import "./zha-network"; import "./zha-node"; +import "./zha-binding"; export class HaConfigZha extends LitElement { - public hass?: HomeAssistant; - public isWide?: boolean; - private _selectedNode?: HassEntity; - private _selectedCluster?: Cluster; + @property() public hass?: HomeAssistant; + @property() public isWide?: boolean; + @property() private _selectedDevice?: ZHADevice; + @property() private _selectedCluster?: Cluster; + @property() private _bindableDevices: ZHADevice[] = []; - static get properties(): PropertyDeclarations { - return { - hass: {}, - isWide: {}, - _selectedCluster: {}, - _selectedNode: {}, - }; + protected updated(changedProperties: PropertyValues): void { + if (changedProperties.has("_selectedDevice")) { + this._fetchBindableDevices(); + } + super.update(changedProperties); } protected render(): TemplateResult | void { @@ -57,25 +57,35 @@ export class HaConfigZha extends LitElement { .isWide="${this.isWide}" .hass="${this.hass}" @zha-cluster-selected="${this._onClusterSelected}" - @zha-node-selected="${this._onNodeSelected}" + @zha-node-selected="${this._onDeviceSelected}" > ${this._selectedCluster ? html` ` : ""} + ${this._selectedDevice && this._bindableDevices.length > 0 + ? html` + + ` + : ""} `; } @@ -86,13 +96,24 @@ export class HaConfigZha extends LitElement { this._selectedCluster = selectedClusterEvent.detail.cluster; } - private _onNodeSelected( - selectedNodeEvent: HASSDomEvent + private _onDeviceSelected( + selectedNodeEvent: HASSDomEvent ): void { - this._selectedNode = selectedNodeEvent.detail.node; + this._selectedDevice = selectedNodeEvent.detail.node; this._selectedCluster = undefined; } + private async _fetchBindableDevices(): Promise { + if (this._selectedDevice && this.hass) { + this._bindableDevices = (await fetchBindableDevices( + this.hass, + this._selectedDevice!.ieee + )).sort((a, b) => { + return a.name.localeCompare(b.name); + }); + } + } + static get styles(): CSSResult[] { return [haStyle]; } diff --git a/src/panels/config/zha/types.ts b/src/panels/config/zha/types.ts index 1f006964d7..5e531c98aa 100644 --- a/src/panels/config/zha/types.ts +++ b/src/panels/config/zha/types.ts @@ -1,4 +1,4 @@ -import { ZHADeviceEntity, Cluster } from "../../../data/zha"; +import { ZHADevice, Cluster } from "../../../data/zha"; export interface PickerTarget extends EventTarget { selected: number; @@ -34,8 +34,8 @@ export interface IssueCommandServiceData { command_type: string; } -export interface ZHANodeSelectedParams { - node: ZHADeviceEntity; +export interface ZHADeviceSelectedParams { + node: ZHADevice; } export interface ZHAClusterSelectedParams { diff --git a/src/panels/config/zha/zha-binding.ts b/src/panels/config/zha/zha-binding.ts new file mode 100644 index 0000000000..3394262a83 --- /dev/null +++ b/src/panels/config/zha/zha-binding.ts @@ -0,0 +1,211 @@ +import { + html, + LitElement, + property, + PropertyValues, + TemplateResult, + CSSResult, + css, + customElement, +} from "lit-element"; +import "@polymer/paper-card/paper-card"; +import "../../../components/buttons/ha-call-service-button"; +import "../../../components/ha-service-description"; +import { ZHADevice, bindDevices, unbindDevices } from "../../../data/zha"; +import { haStyle } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import "../ha-config-section"; +import { ItemSelectedEvent } from "./types"; + +@customElement("zha-binding-control") +export class ZHABindingControl extends LitElement { + @property() public hass?: HomeAssistant; + @property() public isWide?: boolean; + @property() public selectedDevice?: ZHADevice; + @property() private _showHelp: boolean = false; + @property() private _bindTargetIndex: number = -1; + @property() private bindableDevices: ZHADevice[] = []; + private _deviceToBind?: ZHADevice; + + protected updated(changedProperties: PropertyValues): void { + if (changedProperties.has("selectedDevice")) { + this._bindTargetIndex = -1; + } + super.update(changedProperties); + } + + protected render(): TemplateResult | void { + return html` + +
+ Device Binding + + +
+ Bind and unbind devices. + + +
+ + + ${this.bindableDevices.map( + (device) => html` + ${device.name} + ` + )} + + +
+ ${this._showHelp + ? html` +
+ Select a device to issue a bind command. +
+ ` + : ""} +
+ Bind + ${this._showHelp + ? html` +
+ Bind devices. +
+ ` + : ""} + Unbind + ${this._showHelp + ? html` +
+ Unbind devices. +
+ ` + : ""} +
+
+
+ `; + } + + private _bindTargetIndexChanged(event: ItemSelectedEvent): void { + this._bindTargetIndex = event.target!.selected; + this._deviceToBind = + this._bindTargetIndex === -1 + ? undefined + : this.bindableDevices[this._bindTargetIndex]; + } + + private _onHelpTap(): void { + this._showHelp = !this._showHelp; + } + + private async _onBindDevicesClick(): Promise { + if (this.hass && this._deviceToBind && this.selectedDevice) { + await bindDevices( + this.hass, + this.selectedDevice.ieee, + this._deviceToBind.ieee + ); + } + } + + private async _onUnbindDevicesClick(): Promise { + if (this.hass && this._deviceToBind && this.selectedDevice) { + await unbindDevices( + this.hass, + this.selectedDevice.ieee, + this._deviceToBind.ieee + ); + } + } + + static get styles(): CSSResult[] { + return [ + haStyle, + css` + .flex { + -ms-flex: 1 1 0.000000001px; + -webkit-flex: 1; + flex: 1; + -webkit-flex-basis: 0.000000001px; + flex-basis: 0.000000001px; + } + + .content { + margin-top: 24px; + } + + paper-card { + display: block; + margin: 0 auto; + max-width: 600px; + } + + .card-actions.warning ha-call-service-button { + color: var(--google-red-500); + } + + .command-picker { + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + -ms-flex-direction: row; + -webkit-flex-direction: row; + flex-direction: row; + -ms-flex-align: center; + -webkit-align-items: center; + align-items: center; + padding-left: 28px; + padding-right: 28px; + padding-bottom: 10px; + } + + .input-text { + padding-left: 28px; + padding-right: 28px; + padding-bottom: 10px; + } + + .sectionHeader { + position: relative; + } + + .helpText { + color: grey; + padding: 16px; + } + + .toggle-help-icon { + position: absolute; + top: -6px; + right: 0; + color: var(--primary-color); + } + + ha-service-description { + display: block; + color: grey; + } + + [hidden] { + display: none; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-binding-control": ZHABindingControl; + } +}