Expand ZHA configuration panel (#2421)

* node panel

* cleanup

* add entities

* entities split into own element

* cleanup

* add clusters

* cleanup and attributes and commands

* start cluster attributes

* finish cluster attribute interaction

* cluster command panel

* scope cleanup

* fix parseInt

* guards and template cleanup

* cleanup

* cleanup

* fix missing button

* type info

* make names consistent

* cleanup - review comments

* split out fetch attributes command - review comment

* move _computeReadAttributeServiceData - review comment

* move readAttributeValue to zha.ts - review comment

* move fetchCommandsForCluster - review comment

* move fetchClustersForZhaNode - review comment

* move fetchEntitiesForZhaNode - review comment

* style changes - review comment

* cleanup - review comments

* fully sort imports

* use updated vs update - review comment

* remove unnecessary ids - review comment

* remove empty attributes - review comment

* fix read attribute value - review comment

* switch reconfigure to web socket command - review comment
This commit is contained in:
David F. Mulcahey 2019-01-11 15:07:58 -05:00 committed by Paulus Schoutsen
parent e96c9daad6
commit 6d43c9e86a
9 changed files with 1517 additions and 25 deletions

105
src/data/zha.ts Normal file
View File

@ -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<void> =>
hass.callWS({
type: "zha/nodes/reconfigure",
ieee: ieeeAddress,
});
export const fetchAttributesForCluster = (
hass: HomeAssistant,
entityId: string,
ieeeAddress: string,
clusterId: number,
clusterType: string
): Promise<Attribute[]> =>
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<string> => {
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<Command[]> =>
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<Cluster[]> =>
hass.callWS({
type: "zha/entities/clusters",
entity_id: entityId,
ieee: ieeeAddress,
});
export const fetchEntitiesForZhaNode = (
hass: HomeAssistant
): Promise<ZHAEntities> =>
hass.callWS({
type: "zha/entities",
});

View File

@ -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()}
<ha-app-layout has-scrolling-region="">
<app-header slot="header" fixed="">
<ha-app-layout>
<app-header slot="header">
<app-toolbar>
<paper-icon-button
icon="hass:arrow-left"
@click="${this._onBackTapped}"
></paper-icon-button>
<div main-title>Zigbee Home Automation</div>
</app-toolbar>
</app-header>
<zha-network
id="zha-network"
.is-wide="${this.isWide}"
.isWide="${this.isWide}"
.hass="${this.hass}"
></zha-network>
<zha-node
.isWide="${this.isWide}"
.hass="${this.hass}"
@zha-cluster-selected="${this._onClusterSelected}"
@zha-node-selected="${this._onNodeSelected}"
@zha-entity-selected="${this._onEntitySelected}"
></zha-node>
${
this._selectedCluster
? html`
<zha-cluster-attributes
.isWide="${this.isWide}"
.hass="${this.hass}"
.selectedNode="${this._selectedNode}"
.selectedEntity="${this._selectedEntity}"
.selectedCluster="${this._selectedCluster}"
></zha-cluster-attributes>
<zha-cluster-commands
.isWide="${this.isWide}"
.hass="${this.hass}"
.selectedNode="${this._selectedNode}"
.selectedEntity="${this._selectedEntity}"
.selectedCluster="${this._selectedCluster}"
></zha-cluster-commands>
`
: ""
}
</ha-app-layout>
`;
}
private _onClusterSelected(
selectedClusterEvent: HASSDomEvent<ZHAClusterSelectedParams>
): void {
this._selectedCluster = selectedClusterEvent.detail.cluster;
}
private _onNodeSelected(
selectedNodeEvent: HASSDomEvent<ZHANodeSelectedParams>
): void {
this._selectedNode = selectedNodeEvent.detail.node;
this._selectedCluster = undefined;
this._selectedEntity = undefined;
}
private _onEntitySelected(
selectedEntityEvent: HASSDomEvent<ZHAEntitySelectedParams>
): void {
this._selectedEntity = selectedEntityEvent.detail.entity;
}
private renderStyle(): TemplateResult {
if (!this._haStyle) {
this._haStyle = document.importNode(

View File

@ -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;
}

View File

@ -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()}
<ha-config-section .isWide="${this.isWide}">
<div style="position: relative" slot="header">
<span>Cluster Attributes</span>
<paper-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
>
</paper-icon-button>
</div>
<span slot="introduction">View and edit cluster attributes.</span>
<paper-card class="content">
<div class="attribute-picker">
<paper-dropdown-menu
label="Attributes of the selected cluster"
class="flex"
>
<paper-listbox
slot="dropdown-content"
.selected="${this._selectedAttributeIndex}"
@iron-select="${this._selectedAttributeChanged}"
>
${
this._attributes.map(
(entry) => html`
<paper-item
>${entry.name + " (id: " + entry.id + ")"}</paper-item
>
`
)
}
</paper-listbox>
</paper-dropdown-menu>
</div>
${
this.showHelp
? html`
<div style="color: grey; padding: 16px">
Select an attribute to view or set its value
</div>
`
: ""
}
${
this._selectedAttributeIndex !== -1
? this._renderAttributeInteractions()
: ""
}
</paper-card>
</ha-config-section>
`;
}
private _renderAttributeInteractions(): TemplateResult {
return html`
<div class="input-text">
<paper-input
label="Value"
type="string"
.value="${this._attributeValue}"
@value-changed="${this._onAttributeValueChanged}"
placeholder="Value"
></paper-input>
</div>
<div class="input-text">
<paper-input
label="Manufacturer code override"
type="number"
.value="${this._manufacturerCodeOverride}"
@value-changed="${this._onManufacturerCodeOverrideChanged}"
placeholder="Value"
></paper-input>
</div>
<div class="card-actions">
<paper-button @click="${this._onGetZigbeeAttributeClick}"
>Get Zigbee Attribute</paper-button
>
<ha-call-service-button
.hass="${this.hass}"
domain="zha"
service="set_zigbee_cluster_attribute"
.serviceData="${this._setAttributeServiceData}"
>Set Zigbee Attribute</ha-call-service-button
>
${
this.showHelp
? html`
<ha-service-description
.hass="${this.hass}"
domain="zha"
service="set_zigbee_cluster_attribute"
></ha-service-description>
`
: ""
}
</div>
`;
}
private async _fetchAttributesForCluster(): Promise<void> {
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<void> {
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}
<style>
.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);
}
.attribute-picker {
@apply --layout-horizontal;
@apply --layout-center-center;
padding-left: 28px;
padding-right: 28px;
padding-bottom: 10px;
}
.input-text {
padding-left: 28px;
padding-right: 28px;
padding-bottom: 10px;
}
.toggle-help-icon {
position: absolute;
top: -6px;
right: 0;
color: var(--primary-color);
}
ha-service-description {
display: block;
color: grey;
}
[hidden] {
display: none;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-cluster-attributes": ZHAClusterAttributes;
}
}
customElements.define("zha-cluster-attributes", ZHAClusterAttributes);

View File

@ -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()}
<ha-config-section .isWide="${this.isWide}">
<div class="sectionHeader" slot="header">
<span>Cluster Commands</span>
<paper-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
>
</paper-icon-button>
</div>
<span slot="introduction">View and issue cluster commands.</span>
<paper-card class="content">
<div class="command-picker">
<paper-dropdown-menu
label="Commands of the selected cluster"
class="flex"
>
<paper-listbox
slot="dropdown-content"
.selected="${this._selectedCommandIndex}"
@iron-select="${this._selectedCommandChanged}"
>
${
this._commands.map(
(entry) => html`
<paper-item
>${entry.name + " (id: " + entry.id + ")"}</paper-item
>
`
)
}
</paper-listbox>
</paper-dropdown-menu>
</div>
${
this._showHelp
? html`
<div class="helpText">Select a command to interact with</div>
`
: ""
}
${
this._selectedCommandIndex !== -1
? html`
<div class="input-text">
<paper-input
label="Manufacturer code override"
type="number"
.value="${this._manufacturerCodeOverride}"
@value-changed="${
this._onManufacturerCodeOverrideChanged
}"
placeholder="Value"
></paper-input>
</div>
<div class="card-actions">
<ha-call-service-button
.hass="${this.hass}"
domain="zha"
service="issue_zigbee_cluster_command"
.serviceData="${this._issueClusterCommandServiceData}"
>Issue Zigbee Command</ha-call-service-button
>
${
this._showHelp
? html`
<ha-service-description
.hass="${this.hass}"
domain="zha"
service="issue_zigbee_cluster_command"
></ha-service-description>
`
: ""
}
</div>
`
: ""
}
</paper-card>
</ha-config-section>
`;
}
private async _fetchCommandsForCluster(): Promise<void> {
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}
<style>
.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 {
@apply --layout-horizontal;
@apply --layout-center-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;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-cluster-commands": ZHAClusterCommands;
}
}
customElements.define("zha-cluster-commands", ZHAClusterCommands);

View File

@ -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()}
<div class="node-picker">
<paper-dropdown-menu label="Clusters" class="flex">
<paper-listbox
slot="dropdown-content"
.selected="${this._selectedClusterIndex}"
@iron-select="${this._selectedClusterChanged}"
>
${
this._clusters.map(
(entry) => html`
<paper-item>${computeClusterKey(entry)}</paper-item>
`
)
}
</paper-listbox>
</paper-dropdown-menu>
</div>
${
this.showHelp
? html`
<div class="helpText">
Select cluster to view attributes and commands
</div>
`
: ""
}
`;
}
private async _fetchClustersForZhaNode(): Promise<void> {
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}
<style>
.node-picker {
@apply --layout-horizontal;
@apply --layout-center-center;
padding-left: 28px;
padding-right: 28px;
padding-bottom: 10px;
}
.helpText {
color: grey;
padding: 16px;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-cluster": ZHAClusters;
}
}
customElements.define("zha-clusters", ZHAClusters);

View File

@ -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()}
<div class="node-picker">
<paper-dropdown-menu label="Entities" class="flex">
<paper-listbox
slot="dropdown-content"
.selected="${this._selectedEntityIndex}"
@iron-select="${this._selectedEntityChanged}"
>
${
this._entities.map(
(entry) => html`
<paper-item>${entry.entity_id}</paper-item>
`
)
}
</paper-listbox>
</paper-dropdown-menu>
</div>
${
this.showHelp
? html`
<div class="helpText">
Select entity to view per-entity options
</div>
`
: ""
}
${
this._selectedEntityIndex !== -1
? html`
<div class="actions">
<paper-button @click="${this._showEntityInformation}"
>Entity Information</paper-button
>
</div>
`
: ""
}
`;
}
private async _fetchEntitiesForZhaNode(): Promise<void> {
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}
<style>
.node-picker {
@apply --layout-horizontal;
@apply --layout-center-center;
padding-left: 28px;
padding-right: 28px;
padding-bottom: 10px;
}
.actions {
border-top: 1px solid #e8e8e8;
padding: 5px 16px;
position: relative;
}
.actions paper-button:not([disabled]) {
color: var(--primary-color);
font-weight: 500;
}
.helpText {
color: grey;
padding: 16px;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-entities": ZHAEntities;
}
}
customElements.define("zha-entities", ZHAEntities);

View File

@ -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()}
<ha-config-section .is-wide="${this.isWide}">
<ha-config-section .isWide="${this.isWide}">
<div style="position: relative" slot="header">
<span>Zigbee Home Automation network management</span>
<span>Network Management</span>
<paper-icon-button class="toggle-help-icon" @click="${
this._onHelpTap
}" icon="hass:help-circle"></paper-icon-button>
@ -49,7 +48,7 @@ export class ZHANetwork extends LitElement {
this.hass
}" domain="zha" service="permit">Permit</ha-call-service-button>
${
this.showDescription
this._showHelp
? html`
<ha-service-description
.hass="${this.hass}"
@ -65,7 +64,7 @@ export class ZHANetwork extends LitElement {
}
private _onHelpTap(): void {
this.showDescription = !this.showDescription;
this._showHelp = !this._showHelp;
}
private renderStyle(): TemplateResult {

View File

@ -0,0 +1,321 @@
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 "@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, HASSDomEvent } from "../../../common/dom/fire_event";
import computeStateName from "../../../common/entity/compute_state_name";
import sortByName from "../../../common/entity/states_sort_by_name";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import "../../../resources/ha-style";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import {
ItemSelectedEvent,
NodeServiceData,
ZHAEntitySelectedParams,
} from "./types";
import "./zha-clusters";
import "./zha-entities";
import { reconfigureNode } from "../../../data/zha";
declare global {
// for fire event
interface HASSDomEvents {
"zha-node-selected": {
node?: HassEntity;
};
}
}
export class ZHANode extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
private _showHelp: boolean;
private _selectedNodeIndex: number;
private _selectedNode?: HassEntity;
private _selectedEntity?: HassEntity;
private _serviceData?: {};
private _haStyle?: DocumentFragment;
private _ironFlex?: DocumentFragment;
private _nodes: HassEntity[];
constructor() {
super();
this._showHelp = false;
this._selectedNodeIndex = -1;
this._nodes = [];
}
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
_showHelp: {},
_selectedNodeIndex: {},
_selectedNode: {},
_serviceData: {},
_selectedEntity: {},
};
}
protected render(): TemplateResult {
this._nodes = this._computeNodes(this.hass);
return html`
${this.renderStyle()}
<ha-config-section .isWide="${this.isWide}">
<div class="sectionHeader" slot="header">
<span>Node Management</span>
<paper-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
></paper-icon-button>
</div>
<span slot="introduction">
Run ZHA commands that affect a single node. Pick a node to see a list
of available commands. <br /><br />Note: Sleepy (battery powered)
devices need to be awake when executing commands against them. You can
generally wake a sleepy device by triggering it. <br /><br />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.
</span>
<paper-card class="content">
<div class="node-picker">
<paper-dropdown-menu label="Nodes" class="flex">
<paper-listbox
slot="dropdown-content"
@iron-select="${this._selectedNodeChanged}"
>
${
this._nodes.map(
(entry) => html`
<paper-item
>${this._computeSelectCaption(entry)}</paper-item
>
`
)
}
</paper-listbox>
</paper-dropdown-menu>
</div>
${
this._showHelp
? html`
<div class="helpText">
Select node to view per-node options
</div>
`
: ""
}
${this._selectedNodeIndex !== -1 ? this._renderNodeActions() : ""}
${this._selectedNodeIndex !== -1 ? this._renderEntities() : ""}
${this._selectedEntity ? this._renderClusters() : ""}
</paper-card>
</ha-config-section>
`;
}
private _renderNodeActions(): TemplateResult {
return html`
<div class="card-actions">
<paper-button @click="${this._showNodeInformation}"
>Node Information</paper-button
>
<paper-button @click="${this._onReconfigureNodeClick}"
>Reconfigure Node</paper-button
>
${
this._showHelp
? html`
<ha-service-description
.hass="${this.hass}"
domain="zha"
service="reconfigure_device"
/>
`
: ""
}
<ha-call-service-button
.hass="${this.hass}"
domain="zha"
service="remove"
.serviceData="${this._serviceData}"
>Remove Node</ha-call-service-button
>
${
this._showHelp
? html`
<ha-service-description
.hass="${this.hass}"
domain="zha"
service="remove"
/>
`
: ""
}
</div>
`;
}
private _renderEntities(): TemplateResult {
return html`
<zha-entities
.hass="${this.hass}"
.selectedNode="${this._selectedNode}"
.showHelp="${this._showHelp}"
@zha-entity-selected="${this._onEntitySelected}"
></zha-entities>
`;
}
private _renderClusters(): TemplateResult {
return html`
<zha-clusters
.hass="${this.hass}"
.selectedEntity="${this._selectedEntity}"
.showHelp="${this._showHelp}"
></zha-clusters>
`;
}
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<void> {
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<ZHAEntitySelectedParams>
): 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}
<style>
.content {
margin-top: 24px;
}
.node-info {
margin-left: 16px;
}
.sectionHeader {
position: relative;
}
.help-text {
padding-left: 28px;
padding-right: 28px;
}
.helpText {
color: grey;
padding: 16px;
}
paper-card {
display: block;
margin: 0 auto;
max-width: 600px;
}
.node-picker {
@apply --layout-horizontal;
@apply --layout-center-center;
padding-left: 28px;
padding-right: 28px;
padding-bottom: 10px;
}
ha-service-description {
display: block;
color: grey;
}
[hidden] {
display: none;
}
.toggle-help-icon {
position: absolute;
top: 6px;
right: 0;
color: var(--primary-color);
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-node": ZHANode;
}
}
customElements.define("zha-node", ZHANode);