diff --git a/src/data/zha.ts b/src/data/zha.ts new file mode 100644 index 0000000000..5752b620b1 --- /dev/null +++ b/src/data/zha.ts @@ -0,0 +1,105 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { HomeAssistant } from "../types"; + +export interface ZHADeviceEntity extends HassEntity { + device_info?: { + identifiers: any[]; + }; +} + +export interface ZHAEntities { + [key: string]: HassEntity[]; +} + +export interface Attribute { + name: string; + id: number; +} + +export interface Cluster { + name: string; + id: number; + type: string; +} + +export interface Command { + name: string; + id: number; + type: string; +} + +export interface ReadAttributeServiceData { + entity_id: string; + cluster_id: number; + cluster_type: string; + attribute: number; + manufacturer: number; +} + +export const reconfigureNode = ( + hass: HomeAssistant, + ieeeAddress: string +): Promise => + hass.callWS({ + type: "zha/nodes/reconfigure", + ieee: ieeeAddress, + }); + +export const fetchAttributesForCluster = ( + hass: HomeAssistant, + entityId: string, + ieeeAddress: string, + clusterId: number, + clusterType: string +): Promise => + hass.callWS({ + type: "zha/entities/clusters/attributes", + entity_id: entityId, + ieee: ieeeAddress, + cluster_id: clusterId, + cluster_type: clusterType, + }); + +export const readAttributeValue = ( + hass: HomeAssistant, + data: ReadAttributeServiceData +): Promise => { + const serviceData = { + type: "zha/entities/clusters/attributes/value", + }; + Object.assign(serviceData, data); + return hass.callWS(serviceData); +}; + +export const fetchCommandsForCluster = ( + hass: HomeAssistant, + entityId: string, + ieeeAddress: string, + clusterId: number, + clusterType: string +): Promise => + hass!.callWS({ + type: "zha/entities/clusters/commands", + entity_id: entityId, + ieee: ieeeAddress, + cluster_id: clusterId, + cluster_type: clusterType, + }); + +export const fetchClustersForZhaNode = ( + hass: HomeAssistant, + entityId: string, + ieeeAddress: string +): Promise => + hass.callWS({ + type: "zha/entities/clusters", + entity_id: entityId, + ieee: ieeeAddress, + }); + +export const fetchEntitiesForZhaNode = ( + hass: HomeAssistant +): Promise => + hass.callWS({ + type: "zha/entities", + }); diff --git a/src/panels/config/zha/ha-config-zha.ts b/src/panels/config/zha/ha-config-zha.ts index 488c45c43f..a307e14850 100755 --- a/src/panels/config/zha/ha-config-zha.ts +++ b/src/panels/config/zha/ha-config-zha.ts @@ -1,51 +1,115 @@ -import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; -import { TemplateResult } from "lit-html"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; -import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import { HomeAssistant } from "../../../types"; - +import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; +import "@polymer/paper-icon-button/paper-icon-button"; +import { HassEntity } from "home-assistant-js-websocket"; +import { TemplateResult } from "lit-html"; +import { HASSDomEvent } from "../../../common/dom/fire_event"; +import { Cluster } from "../../../data/zha"; import "../../../layouts/ha-app-layout"; import "../../../resources/ha-style"; - +import { HomeAssistant } from "../../../types"; +import { + ZHAClusterSelectedParams, + ZHAEntitySelectedParams, + ZHANodeSelectedParams, +} from "./types"; +import "./zha-cluster-attributes"; +import "./zha-cluster-commands"; import "./zha-network"; +import "./zha-node"; export class HaConfigZha extends LitElement { public hass?: HomeAssistant; public isWide?: boolean; private _haStyle?: DocumentFragment; private _ironFlex?: DocumentFragment; + private _selectedNode?: HassEntity; + private _selectedCluster?: Cluster; + private _selectedEntity?: HassEntity; static get properties(): PropertyDeclarations { return { hass: {}, isWide: {}, + _selectedCluster: {}, + _selectedEntity: {}, + _selectedNode: {}, }; } protected render(): TemplateResult { return html` ${this.renderStyle()} - - + + +
Zigbee Home Automation
+ + + ${ + this._selectedCluster + ? html` + + + + ` + : "" + }
`; } + private _onClusterSelected( + selectedClusterEvent: HASSDomEvent + ): void { + this._selectedCluster = selectedClusterEvent.detail.cluster; + } + + private _onNodeSelected( + selectedNodeEvent: HASSDomEvent + ): void { + this._selectedNode = selectedNodeEvent.detail.node; + this._selectedCluster = undefined; + this._selectedEntity = undefined; + } + + private _onEntitySelected( + selectedEntityEvent: HASSDomEvent + ): void { + this._selectedEntity = selectedEntityEvent.detail.entity; + } + private renderStyle(): TemplateResult { if (!this._haStyle) { this._haStyle = document.importNode( diff --git a/src/panels/config/zha/types.ts b/src/panels/config/zha/types.ts new file mode 100644 index 0000000000..23bc9f74d7 --- /dev/null +++ b/src/panels/config/zha/types.ts @@ -0,0 +1,50 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { ZHADeviceEntity, Cluster } from "../../../data/zha"; + +export interface PickerTarget extends EventTarget { + selected: number; +} + +export interface ItemSelectedEvent { + target?: PickerTarget; +} + +export interface ChangeEvent { + detail?: { + value?: any; + }; + target?: EventTarget; +} + +export interface SetAttributeServiceData { + entity_id: string; + cluster_id: number; + cluster_type: string; + attribute: number; + value: any; + manufacturer: number; +} + +export interface IssueCommandServiceData { + entity_id: string; + cluster_id: number; + cluster_type: string; + command: number; + command_type: string; +} + +export interface ZHAEntitySelectedParams { + entity: HassEntity; +} + +export interface ZHANodeSelectedParams { + node: ZHADeviceEntity; +} + +export interface ZHAClusterSelectedParams { + cluster: Cluster; +} + +export interface NodeServiceData { + ieee_address: string; +} diff --git a/src/panels/config/zha/zha-cluster-attributes.ts b/src/panels/config/zha/zha-cluster-attributes.ts new file mode 100644 index 0000000000..3a18e025fb --- /dev/null +++ b/src/panels/config/zha/zha-cluster-attributes.ts @@ -0,0 +1,329 @@ +import "@polymer/iron-flex-layout/iron-flex-layout-classes"; +import { + html, + LitElement, + PropertyDeclarations, + PropertyValues, +} from "@polymer/lit-element"; +import "@polymer/paper-button/paper-button"; +import "@polymer/paper-card/paper-card"; +import "@polymer/paper-icon-button/paper-icon-button"; +import { HassEntity } from "home-assistant-js-websocket"; +import { TemplateResult } from "lit-html"; +import "../../../components/buttons/ha-call-service-button"; +import "../../../components/ha-service-description"; +import { + Attribute, + Cluster, + fetchAttributesForCluster, + ReadAttributeServiceData, + readAttributeValue, + ZHADeviceEntity, +} from "../../../data/zha"; +import "../../../resources/ha-style"; +import { HomeAssistant } from "../../../types"; +import "../ha-config-section"; +import { + ChangeEvent, + ItemSelectedEvent, + SetAttributeServiceData, +} from "./types"; + +export class ZHAClusterAttributes extends LitElement { + public hass?: HomeAssistant; + public isWide?: boolean; + public showHelp: boolean; + public selectedNode?: HassEntity; + public selectedEntity?: ZHADeviceEntity; + public selectedCluster?: Cluster; + private _haStyle?: DocumentFragment; + private _ironFlex?: DocumentFragment; + private _attributes: Attribute[]; + private _selectedAttributeIndex: number; + private _attributeValue?: any; + private _manufacturerCodeOverride?: string | number; + private _setAttributeServiceData?: SetAttributeServiceData; + + constructor() { + super(); + this.showHelp = false; + this._selectedAttributeIndex = -1; + this._attributes = []; + this._attributeValue = ""; + } + + static get properties(): PropertyDeclarations { + return { + hass: {}, + isWide: {}, + showHelp: {}, + selectedNode: {}, + selectedEntity: {}, + selectedCluster: {}, + _attributes: {}, + _selectedAttributeIndex: {}, + _attributeValue: {}, + _manufacturerCodeOverride: {}, + _setAttributeServiceData: {}, + }; + } + + protected updated(changedProperties: PropertyValues): void { + if (changedProperties.has("selectedCluster")) { + this._attributes = []; + this._selectedAttributeIndex = -1; + this._attributeValue = ""; + this._fetchAttributesForCluster(); + } + super.update(changedProperties); + } + + protected render(): TemplateResult { + return html` + ${this.renderStyle()} + +
+ Cluster Attributes + + +
+ View and edit cluster attributes. + + +
+ + + ${ + this._attributes.map( + (entry) => html` + ${entry.name + " (id: " + entry.id + ")"} + ` + ) + } + + +
+ ${ + this.showHelp + ? html` +
+ Select an attribute to view or set its value +
+ ` + : "" + } + ${ + this._selectedAttributeIndex !== -1 + ? this._renderAttributeInteractions() + : "" + } +
+
+ `; + } + + private _renderAttributeInteractions(): TemplateResult { + return html` +
+ +
+
+ +
+
+ Get Zigbee Attribute + Set Zigbee Attribute + ${ + this.showHelp + ? html` + + ` + : "" + } +
+ `; + } + + private async _fetchAttributesForCluster(): Promise { + if (this.selectedEntity && this.selectedCluster && this.hass) { + this._attributes = await fetchAttributesForCluster( + this.hass, + this.selectedEntity!.entity_id, + this.selectedEntity!.device_info!.identifiers[0][1], + this.selectedCluster!.id, + this.selectedCluster!.type + ); + } + } + + private _computeReadAttributeServiceData(): + | ReadAttributeServiceData + | undefined { + if (!this.selectedEntity || !this.selectedCluster || !this.selectedNode) { + return; + } + return { + entity_id: this.selectedEntity!.entity_id, + cluster_id: this.selectedCluster!.id, + cluster_type: this.selectedCluster!.type, + attribute: this._attributes[this._selectedAttributeIndex].id, + manufacturer: this._manufacturerCodeOverride + ? parseInt(this._manufacturerCodeOverride as string, 10) + : this.selectedNode!.attributes.manufacturer_code, + }; + } + + private _computeSetAttributeServiceData(): + | SetAttributeServiceData + | undefined { + if (!this.selectedEntity || !this.selectedCluster || !this.selectedNode) { + return; + } + return { + entity_id: this.selectedEntity!.entity_id, + cluster_id: this.selectedCluster!.id, + cluster_type: this.selectedCluster!.type, + attribute: this._attributes[this._selectedAttributeIndex].id, + value: this._attributeValue, + manufacturer: this._manufacturerCodeOverride + ? parseInt(this._manufacturerCodeOverride as string, 10) + : this.selectedNode!.attributes.manufacturer_code, + }; + } + + private _onAttributeValueChanged(value: ChangeEvent): void { + this._attributeValue = value.detail!.value; + this._setAttributeServiceData = this._computeSetAttributeServiceData(); + } + + private _onManufacturerCodeOverrideChanged(value: ChangeEvent): void { + this._manufacturerCodeOverride = value.detail!.value; + this._setAttributeServiceData = this._computeSetAttributeServiceData(); + } + + private async _onGetZigbeeAttributeClick(): Promise { + const data = this._computeReadAttributeServiceData(); + if (data && this.hass) { + this._attributeValue = await readAttributeValue(this.hass, data); + } + } + + private _onHelpTap(): void { + this.showHelp = !this.showHelp; + } + + private _selectedAttributeChanged(event: ItemSelectedEvent): void { + this._selectedAttributeIndex = event.target!.selected; + this._attributeValue = ""; + } + + private renderStyle(): TemplateResult { + if (!this._haStyle) { + this._haStyle = document.importNode( + (document.getElementById("ha-style")! + .children[0] as HTMLTemplateElement).content, + true + ); + } + if (!this._ironFlex) { + this._ironFlex = document.importNode( + (document.getElementById("iron-flex")! + .children[0] as HTMLTemplateElement).content, + true + ); + } + return html` + ${this._ironFlex} ${this._haStyle} + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-cluster-attributes": ZHAClusterAttributes; + } +} + +customElements.define("zha-cluster-attributes", ZHAClusterAttributes); diff --git a/src/panels/config/zha/zha-cluster-commands.ts b/src/panels/config/zha/zha-cluster-commands.ts new file mode 100644 index 0000000000..e91d4d1162 --- /dev/null +++ b/src/panels/config/zha/zha-cluster-commands.ts @@ -0,0 +1,282 @@ +import "@polymer/iron-flex-layout/iron-flex-layout-classes"; +import { + html, + LitElement, + PropertyDeclarations, + PropertyValues, +} from "@polymer/lit-element"; +import "@polymer/paper-card/paper-card"; +import { HassEntity } from "home-assistant-js-websocket"; +import { TemplateResult } from "lit-html"; +import "../../../components/buttons/ha-call-service-button"; +import "../../../components/ha-service-description"; +import { + Cluster, + Command, + fetchCommandsForCluster, + ZHADeviceEntity, +} from "../../../data/zha"; +import "../../../resources/ha-style"; +import { HomeAssistant } from "../../../types"; +import "../ha-config-section"; +import { + ChangeEvent, + IssueCommandServiceData, + ItemSelectedEvent, +} from "./types"; + +export class ZHAClusterCommands extends LitElement { + public hass?: HomeAssistant; + public isWide?: boolean; + public selectedNode?: HassEntity; + public selectedEntity?: ZHADeviceEntity; + public selectedCluster?: Cluster; + private _showHelp: boolean; + private _haStyle?: DocumentFragment; + private _ironFlex?: DocumentFragment; + private _commands: Command[]; + private _selectedCommandIndex: number; + private _manufacturerCodeOverride?: number; + private _issueClusterCommandServiceData?: IssueCommandServiceData; + + constructor() { + super(); + this._showHelp = false; + this._selectedCommandIndex = -1; + this._commands = []; + } + + static get properties(): PropertyDeclarations { + return { + hass: {}, + isWide: {}, + selectedNode: {}, + selectedEntity: {}, + selectedCluster: {}, + _showHelp: {}, + _commands: {}, + _selectedCommandIndex: {}, + _manufacturerCodeOverride: {}, + _issueClusterCommandServiceData: {}, + }; + } + + protected updated(changedProperties: PropertyValues): void { + if (changedProperties.has("selectedCluster")) { + this._commands = []; + this._selectedCommandIndex = -1; + this._fetchCommandsForCluster(); + } + super.update(changedProperties); + } + + protected render(): TemplateResult { + return html` + ${this.renderStyle()} + +
+ Cluster Commands + + +
+ View and issue cluster commands. + + +
+ + + ${ + this._commands.map( + (entry) => html` + ${entry.name + " (id: " + entry.id + ")"} + ` + ) + } + + +
+ ${ + this._showHelp + ? html` +
Select a command to interact with
+ ` + : "" + } + ${ + this._selectedCommandIndex !== -1 + ? html` +
+ +
+
+ Issue Zigbee Command + ${ + this._showHelp + ? html` + + ` + : "" + } +
+ ` + : "" + } +
+
+ `; + } + + private async _fetchCommandsForCluster(): Promise { + if (this.selectedEntity && this.selectedCluster && this.hass) { + this._commands = await fetchCommandsForCluster( + this.hass, + this.selectedEntity!.entity_id, + this.selectedEntity!.device_info!.identifiers[0][1], + this.selectedCluster!.id, + this.selectedCluster!.type + ); + } + } + + private _computeIssueClusterCommandServiceData(): + | IssueCommandServiceData + | undefined { + if (!this.selectedEntity || !this.selectedCluster) { + return; + } + return { + entity_id: this.selectedEntity!.entity_id, + cluster_id: this.selectedCluster!.id, + cluster_type: this.selectedCluster!.type, + command: this._commands[this._selectedCommandIndex].id, + command_type: this._commands[this._selectedCommandIndex].type, + }; + } + + private _onManufacturerCodeOverrideChanged(value: ChangeEvent): void { + this._manufacturerCodeOverride = value.detail!.value; + this._issueClusterCommandServiceData = this._computeIssueClusterCommandServiceData(); + } + + private _onHelpTap(): void { + this._showHelp = !this._showHelp; + } + + private _selectedCommandChanged(event: ItemSelectedEvent): void { + this._selectedCommandIndex = event.target!.selected; + this._issueClusterCommandServiceData = this._computeIssueClusterCommandServiceData(); + } + + private renderStyle(): TemplateResult { + if (!this._haStyle) { + this._haStyle = document.importNode( + (document.getElementById("ha-style")! + .children[0] as HTMLTemplateElement).content, + true + ); + } + if (!this._ironFlex) { + this._ironFlex = document.importNode( + (document.getElementById("iron-flex")! + .children[0] as HTMLTemplateElement).content, + true + ); + } + return html` + ${this._ironFlex} ${this._haStyle} + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-cluster-commands": ZHAClusterCommands; + } +} + +customElements.define("zha-cluster-commands", ZHAClusterCommands); diff --git a/src/panels/config/zha/zha-clusters.ts b/src/panels/config/zha/zha-clusters.ts new file mode 100644 index 0000000000..6f1b7bd65f --- /dev/null +++ b/src/panels/config/zha/zha-clusters.ts @@ -0,0 +1,165 @@ +import "@polymer/iron-flex-layout/iron-flex-layout-classes"; +import { + html, + LitElement, + PropertyDeclarations, + PropertyValues, +} from "@polymer/lit-element"; +import "@polymer/paper-card/paper-card"; +import { TemplateResult } from "lit-html"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/buttons/ha-call-service-button"; +import "../../../components/ha-service-description"; +import { + Cluster, + fetchClustersForZhaNode, + ZHADeviceEntity, +} from "../../../data/zha"; +import "../../../resources/ha-style"; +import { HomeAssistant } from "../../../types"; +import "../ha-config-section"; +import { ItemSelectedEvent } from "./types"; + +declare global { + // for fire event + interface HASSDomEvents { + "zha-cluster-selected": { + cluster?: Cluster; + }; + } +} + +const computeClusterKey = (cluster: Cluster): string => { + return `${cluster.name} (id: ${cluster.id}, type: ${cluster.type})`; +}; + +export class ZHAClusters extends LitElement { + public hass?: HomeAssistant; + public isWide?: boolean; + public showHelp: boolean; + public selectedEntity?: ZHADeviceEntity; + private _selectedClusterIndex: number; + private _clusters: Cluster[]; + private _haStyle?: DocumentFragment; + private _ironFlex?: DocumentFragment; + + constructor() { + super(); + this.showHelp = false; + this._selectedClusterIndex = -1; + this._clusters = []; + } + + static get properties(): PropertyDeclarations { + return { + hass: {}, + isWide: {}, + showHelp: {}, + selectedEntity: {}, + _selectedClusterIndex: {}, + _clusters: {}, + }; + } + + protected updated(changedProperties: PropertyValues): void { + if (changedProperties.has("selectedEntity")) { + this._clusters = []; + this._selectedClusterIndex = -1; + fireEvent(this, "zha-cluster-selected", { + cluster: undefined, + }); + this._fetchClustersForZhaNode(); + } + super.update(changedProperties); + } + + protected render(): TemplateResult { + return html` + ${this._renderStyle()} +
+ + + ${ + this._clusters.map( + (entry) => html` + ${computeClusterKey(entry)} + ` + ) + } + + +
+ ${ + this.showHelp + ? html` +
+ Select cluster to view attributes and commands +
+ ` + : "" + } + `; + } + + private async _fetchClustersForZhaNode(): Promise { + if (this.hass) { + this._clusters = await fetchClustersForZhaNode( + this.hass, + this.selectedEntity!.entity_id, + this.selectedEntity!.device_info!.identifiers[0][1] + ); + } + } + + private _selectedClusterChanged(event: ItemSelectedEvent): void { + this._selectedClusterIndex = event.target!.selected; + fireEvent(this, "zha-cluster-selected", { + cluster: this._clusters[this._selectedClusterIndex], + }); + } + + private _renderStyle(): TemplateResult { + if (!this._haStyle) { + this._haStyle = document.importNode( + (document.getElementById("ha-style")! + .children[0] as HTMLTemplateElement).content, + true + ); + } + if (!this._ironFlex) { + this._ironFlex = document.importNode( + (document.getElementById("iron-flex")! + .children[0] as HTMLTemplateElement).content, + true + ); + } + return html` + ${this._ironFlex} ${this._haStyle} + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-cluster": ZHAClusters; + } +} + +customElements.define("zha-clusters", ZHAClusters); diff --git a/src/panels/config/zha/zha-entities.ts b/src/panels/config/zha/zha-entities.ts new file mode 100644 index 0000000000..4e340d3546 --- /dev/null +++ b/src/panels/config/zha/zha-entities.ts @@ -0,0 +1,177 @@ +import "@polymer/iron-flex-layout/iron-flex-layout-classes"; +import { + html, + LitElement, + PropertyDeclarations, + PropertyValues, +} from "@polymer/lit-element"; +import "@polymer/paper-button/paper-button"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-listbox/paper-listbox"; +import { HassEntity } from "home-assistant-js-websocket"; +import { TemplateResult } from "lit-html"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { fetchEntitiesForZhaNode } from "../../../data/zha"; +import "../../../resources/ha-style"; +import { HomeAssistant } from "../../../types"; +import { ItemSelectedEvent } from "./types"; + +declare global { + // for fire event + interface HASSDomEvents { + "zha-entity-selected": { + entity?: HassEntity; + }; + } +} + +export class ZHAEntities extends LitElement { + public hass?: HomeAssistant; + public showHelp?: boolean; + public selectedNode?: HassEntity; + private _selectedEntityIndex: number; + private _entities: HassEntity[]; + private _haStyle?: DocumentFragment; + private _ironFlex?: DocumentFragment; + + constructor() { + super(); + this._entities = []; + this._selectedEntityIndex = -1; + } + + static get properties(): PropertyDeclarations { + return { + hass: {}, + showHelp: {}, + selectedNode: {}, + _selectedEntityIndex: {}, + _entities: {}, + }; + } + + protected updated(changedProperties: PropertyValues): void { + if (changedProperties.has("selectedNode")) { + this._entities = []; + this._selectedEntityIndex = -1; + fireEvent(this, "zha-entity-selected", { + entity: undefined, + }); + this._fetchEntitiesForZhaNode(); + } + super.update(changedProperties); + } + + protected render(): TemplateResult { + return html` + ${this._renderStyle()} +
+ + + ${ + this._entities.map( + (entry) => html` + ${entry.entity_id} + ` + ) + } + + +
+ ${ + this.showHelp + ? html` +
+ Select entity to view per-entity options +
+ ` + : "" + } + ${ + this._selectedEntityIndex !== -1 + ? html` +
+ Entity Information +
+ ` + : "" + } + `; + } + + private async _fetchEntitiesForZhaNode(): Promise { + if (this.hass) { + const fetchedEntities = await fetchEntitiesForZhaNode(this.hass); + this._entities = fetchedEntities[this.selectedNode!.attributes.ieee]; + } + } + + private _selectedEntityChanged(event: ItemSelectedEvent): void { + this._selectedEntityIndex = event.target!.selected; + fireEvent(this, "zha-entity-selected", { + entity: this._entities[this._selectedEntityIndex], + }); + } + + private _showEntityInformation(): void { + fireEvent(this, "hass-more-info", { + entityId: this._entities[this._selectedEntityIndex].entity_id, + }); + } + + private _renderStyle(): TemplateResult { + if (!this._haStyle) { + this._haStyle = document.importNode( + (document.getElementById("ha-style")! + .children[0] as HTMLTemplateElement).content, + true + ); + } + if (!this._ironFlex) { + this._ironFlex = document.importNode( + (document.getElementById("iron-flex")! + .children[0] as HTMLTemplateElement).content, + true + ); + } + return html` + ${this._ironFlex} ${this._haStyle} + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-entities": ZHAEntities; + } +} + +customElements.define("zha-entities", ZHAEntities); diff --git a/src/panels/config/zha/zha-network.ts b/src/panels/config/zha/zha-network.ts index 79c6e2f9e9..c83b5feb6f 100644 --- a/src/panels/config/zha/zha-network.ts +++ b/src/panels/config/zha/zha-network.ts @@ -1,42 +1,41 @@ -import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; -import { TemplateResult } from "lit-html"; -import "@polymer/paper-button/paper-button"; -import "@polymer/paper-icon-button/paper-icon-button"; -import "@polymer/paper-card/paper-card"; import "@polymer/iron-flex-layout/iron-flex-layout-classes"; +import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; +import "@polymer/paper-button/paper-button"; +import "@polymer/paper-card/paper-card"; +import "@polymer/paper-icon-button/paper-icon-button"; +import { TemplateResult } from "lit-html"; import "../../../components/buttons/ha-call-service-button"; import "../../../components/ha-service-description"; -import "../ha-config-section"; - -import { HomeAssistant } from "../../../types"; import "../../../resources/ha-style"; +import { HomeAssistant } from "../../../types"; +import "../ha-config-section"; export class ZHANetwork extends LitElement { public hass?: HomeAssistant; public isWide?: boolean; - public showDescription: boolean; + private _showHelp: boolean; private _haStyle?: DocumentFragment; private _ironFlex?: DocumentFragment; constructor() { super(); - this.showDescription = false; + this._showHelp = false; } static get properties(): PropertyDeclarations { return { hass: {}, isWide: {}, - showDescription: {}, + _showHelp: {}, }; } protected render(): TemplateResult { return html` ${this.renderStyle()} - +
- Zigbee Home Automation network management + Network Management @@ -49,7 +48,7 @@ export class ZHANetwork extends LitElement { this.hass }" domain="zha" service="permit">Permit ${ - this.showDescription + this._showHelp ? html` +
+ Node Management + +
+ + Run ZHA commands that affect a single node. Pick a node to see a list + of available commands.

Note: Sleepy (battery powered) + devices need to be awake when executing commands against them. You can + generally wake a sleepy device by triggering it.

Some + devices such as Xiaomi sensors have a wake up button that you can + press at ~5 second intervals that keep devices awake while you + interact with them. +
+ +
+ + + ${ + this._nodes.map( + (entry) => html` + ${this._computeSelectCaption(entry)} + ` + ) + } + + +
+ ${ + this._showHelp + ? html` +
+ Select node to view per-node options +
+ ` + : "" + } + ${this._selectedNodeIndex !== -1 ? this._renderNodeActions() : ""} + ${this._selectedNodeIndex !== -1 ? this._renderEntities() : ""} + ${this._selectedEntity ? this._renderClusters() : ""} +
+ + `; + } + + private _renderNodeActions(): TemplateResult { + return html` +
+ Node Information + Reconfigure Node + ${ + this._showHelp + ? html` + + ` + : "" + } + Remove Node + ${ + this._showHelp + ? html` + + ` + : "" + } +
+ `; + } + + private _renderEntities(): TemplateResult { + return html` + + `; + } + + private _renderClusters(): TemplateResult { + return html` + + `; + } + + private _onHelpTap(): void { + this._showHelp = !this._showHelp; + } + + private _selectedNodeChanged(event: ItemSelectedEvent): void { + this._selectedNodeIndex = event!.target!.selected; + this._selectedNode = this._nodes[this._selectedNodeIndex]; + this._selectedEntity = undefined; + fireEvent(this, "zha-node-selected", { node: this._selectedNode }); + this._serviceData = this._computeNodeServiceData(); + } + + private async _onReconfigureNodeClick(): Promise { + if (this.hass) { + await reconfigureNode(this.hass, this._selectedNode!.attributes.ieee); + } + } + + private _showNodeInformation(): void { + fireEvent(this, "hass-more-info", { + entityId: this._selectedNode!.entity_id, + }); + } + + private _computeNodeServiceData(): NodeServiceData { + return { + ieee_address: this._selectedNode!.attributes.ieee, + }; + } + + private _computeSelectCaption(stateObj: HassEntity): string { + return ( + computeStateName(stateObj) + " (Node:" + stateObj.attributes.ieee + ")" + ); + } + + private _computeNodes(hass?: HomeAssistant): HassEntity[] { + if (hass) { + return Object.keys(hass.states) + .map((key) => hass.states[key]) + .filter((ent) => ent.entity_id.match("zha[.]")) + .sort(sortByName); + } else { + return []; + } + } + + private _onEntitySelected( + entitySelectedEvent: HASSDomEvent + ): void { + this._selectedEntity = entitySelectedEvent.detail.entity; + } + + private renderStyle(): TemplateResult { + if (!this._haStyle) { + this._haStyle = document.importNode( + (document.getElementById("ha-style")! + .children[0] as HTMLTemplateElement).content, + true + ); + } + if (!this._ironFlex) { + this._ironFlex = document.importNode( + (document.getElementById("iron-flex")! + .children[0] as HTMLTemplateElement).content, + true + ); + } + return html` + ${this._ironFlex} ${this._haStyle} + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-node": ZHANode; + } +} + +customElements.define("zha-node", ZHANode);