ZHA add devices page (#2969)

* zha add device page

add device join dialog stub

update dialog stub

fix spinner

add messages and devices to dialog

dialog updates

update dialog

update dialog

add debug info

fix reference

add header

update dialog

test zha gateway message subscription

add device join dialog stub

add messages and devices to dialog

dialog updates

update dialog

add debug info

update dialog

start transitioning to a page instead of a dialog

fix import

subpage

update router

remove old dialog handle

remove dialog parts

make add button call navigate

change extract page

add devices page

cleanup

* update device join page

* auto scroll log

* update css and add device page layout

* fix padding

* fix missing imports

* fix imports

* add -> permit

* left justify device cards to prevent jumping

* conditionally display entity ids

* cleanup

* fix vertical alignment

* review comments

* fix manufacturer overrides
This commit is contained in:
David F. Mulcahey 2019-03-25 23:26:32 -04:00 committed by Ian Richardson
parent 435b7d9cee
commit 669358bf1a
14 changed files with 822 additions and 319 deletions

View File

@ -16,6 +16,7 @@ export interface ZHADevice {
manufacturer_code: number;
device_reg_id: string;
user_given_name: string;
area_id: string;
}
export interface Attribute {
@ -42,7 +43,7 @@ export interface ReadAttributeServiceData {
cluster_id: number;
cluster_type: string;
attribute: number;
manufacturer: number;
manufacturer?: number;
}
export const reconfigureNode = (

View File

@ -73,9 +73,9 @@ class HaPanelConfig extends HassRouterPage {
import(/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users"),
},
zha: {
tag: "ha-config-zha",
tag: "zha-config-panel",
load: () =>
import(/* webpackChunkName: "panel-config-zha" */ "./zha/ha-config-zha"),
import(/* webpackChunkName: "panel-config-zha" */ "./zha/zha-config-panel"),
},
zwave: {
tag: "ha-config-zwave",

View File

@ -1,26 +1,26 @@
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "../../../components/ha-paper-icon-button-arrow-prev";
import "../../../layouts/hass-subpage";
import "./zha-binding";
import "./zha-cluster-attributes";
import "./zha-cluster-commands";
import "./zha-network";
import "./zha-node";
import "@polymer/paper-icon-button/paper-icon-button";
import {
CSSResult,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
CSSResult,
} from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { Cluster, ZHADevice, fetchBindableDevices } from "../../../data/zha";
import "../../../layouts/ha-app-layout";
import "../../../components/ha-paper-icon-button-arrow-prev";
import { Cluster, fetchBindableDevices, ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } 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 {
@property() public hass?: HomeAssistant;
@ -38,16 +38,7 @@ export class HaConfigZha extends LitElement {
protected render(): TemplateResult | void {
return html`
<ha-app-layout>
<app-header slot="header">
<app-toolbar>
<ha-paper-icon-button-arrow-prev
@click="${this._onBackTapped}"
></ha-paper-icon-button-arrow-prev>
<div main-title>Zigbee Home Automation</div>
</app-toolbar>
</app-header>
<hass-subpage header="Zigbee Home Automation">
<zha-network
.isWide="${this.isWide}"
.hass="${this.hass}"
@ -86,7 +77,7 @@ export class HaConfigZha extends LitElement {
></zha-binding-control>
`
: ""}
</ha-app-layout>
</hass-subpage>
`;
}
@ -117,10 +108,6 @@ export class HaConfigZha extends LitElement {
static get styles(): CSSResult[] {
return [haStyle];
}
private _onBackTapped(): void {
history.back();
}
}
declare global {

View File

@ -8,6 +8,12 @@ export interface ItemSelectedEvent {
target?: PickerTarget;
}
export interface ZHADeviceRemovedEvent {
detail?: {
device?: ZHADevice;
};
}
export interface ChangeEvent {
detail?: {
value?: any;
@ -22,7 +28,7 @@ export interface SetAttributeServiceData {
cluster_type: string;
attribute: number;
value: any;
manufacturer: number;
manufacturer?: number;
}
export interface IssueCommandServiceData {

View File

@ -0,0 +1,246 @@
import "../../../components/ha-service-description";
import "../../../components/ha-textarea";
import "../../../layouts/hass-subpage";
import "./zha-device-card";
import "@material/mwc-button";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-spinner/paper-spinner";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
@customElement("zha-add-devices-page")
class ZHAAddDevicesPage extends LitElement {
@property() public hass!: HomeAssistant;
@property() public isWide?: boolean;
@property() private _error?: string;
@property() private _discoveredDevices: ZHADevice[] = [];
@property() private _formattedEvents: string = "";
@property() private _active: boolean = false;
@property() private _showHelp: boolean = false;
private _addDevicesTimeoutHandle: any = undefined;
private _subscribed?: Promise<() => Promise<void>>;
public connectedCallback(): void {
super.connectedCallback();
this._subscribe();
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this._unsubscribe();
this._error = undefined;
this._discoveredDevices = [];
this._formattedEvents = "";
}
protected render(): TemplateResult | void {
return html`
<hass-subpage
header="${this.hass!.localize(
"ui.panel.config.zha.add_device_page.header"
)}"
>
${this._active
? html`
<h2>
<paper-spinner
?active="${this._active}"
alt="Searching"
></paper-spinner>
${this.hass!.localize(
"ui.panel.config.zha.add_device_page.spinner"
)}
</h2>
`
: html`
<div class="card-actions">
<mwc-button @click=${this._subscribe} class="search-button">
Search again
</mwc-button>
<paper-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
></paper-icon-button>
${this._showHelp
? html`
<ha-service-description
.hass="${this.hass}"
domain="zha"
service="permit"
class="help-text"
/>
`
: ""}
</div>
`}
${this._error
? html`
<div class="error">${this._error}</div>
`
: ""}
<div class="content-header"></div>
<div class="content">
${this._discoveredDevices.length < 1
? html`
<div class="discovery-text">
<h4>
${this.hass!.localize(
"ui.panel.config.zha.add_device_page.discovery_text"
)}
</h4>
</div>
`
: html`
${this._discoveredDevices.map(
(device) => html`
<zha-device-card
class="card"
.hass="${this.hass}"
.device="${device}"
.narrow="${!this.isWide}"
.showHelp="${this._showHelp}"
.showActions="${!this._active}"
.isJoinPage="${true}"
></zha-device-card>
`
)}
`}
</div>
<ha-textarea class="events" value="${this._formattedEvents}">
</ha-textarea>
</hass-subpage>
`;
}
private _handleMessage(message: any): void {
if (message.type === "log_output") {
this._formattedEvents += message.log_entry.message + "\n";
if (this.shadowRoot) {
const textArea = this.shadowRoot.querySelector("ha-textarea");
if (textArea) {
textArea.scrollTop = textArea.scrollHeight;
}
}
}
if (message.type && message.type === "device_fully_initialized") {
this._discoveredDevices.push(message.device_info);
}
}
private _unsubscribe(): void {
this._active = false;
if (this._addDevicesTimeoutHandle) {
clearTimeout(this._addDevicesTimeoutHandle);
}
if (this._subscribed) {
this._subscribed.then((unsub) => unsub());
this._subscribed = undefined;
}
}
private _subscribe(): void {
this._subscribed = this.hass!.connection.subscribeMessage(
(message) => this._handleMessage(message),
{ type: "zha/devices/permit" }
);
this._active = true;
this._addDevicesTimeoutHandle = setTimeout(
() => this._unsubscribe(),
60000
);
}
private _onHelpTap(): void {
this._showHelp = !this._showHelp;
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.discovery-text,
.content-header {
margin: 16px;
}
.content {
border-top: 1px solid var(--light-primary-color);
min-height: 500px;
display: flex;
flex-wrap: wrap;
padding: 4px;
justify-content: left;
overflow: scroll;
}
.error {
color: var(--google-red-500);
}
paper-spinner {
display: none;
margin-right: 20px;
margin-left: 16px;
}
paper-spinner[active] {
display: block;
float: left;
margin-right: 20px;
margin-left: 16px;
}
.card {
margin-left: 16px;
margin-right: 16px;
margin-bottom: 0px;
margin-top: 10px;
}
.events {
margin: 16px;
border-top: 1px solid var(--light-primary-color);
padding-top: 16px;
min-height: 200px;
max-height: 200px;
overflow: scroll;
}
.toggle-help-icon {
position: absolute;
margin-top: 16px;
margin-right: 16px;
top: -6px;
right: 0;
color: var(--primary-color);
}
ha-service-description {
margin-top: 16px;
margin-left: 16px;
display: block;
color: grey;
}
.search-button {
margin-top: 16px;
margin-left: 16px;
}
.help-text {
color: grey;
padding-left: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-add-devices-page": ZHAAddDevicesPage;
}
}

View File

@ -1,20 +1,26 @@
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import "../ha-config-section";
import "@material/mwc-button/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
customElement,
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 { bindDevices, unbindDevices, ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { ItemSelectedEvent } from "./types";
@customElement("zha-binding-control")

View File

@ -1,17 +1,24 @@
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import "../ha-config-section";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
html,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
CSSResult,
css,
} from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import {
Attribute,
Cluster,
@ -22,13 +29,12 @@ import {
} from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { formatAsPaddedHex } from "./functions";
import {
ChangeEvent,
ItemSelectedEvent,
SetAttributeServiceData,
} from "./types";
import { formatAsPaddedHex } from "./functions";
export class ZHAClusterAttributes extends LitElement {
public hass?: HomeAssistant;
@ -115,7 +121,7 @@ export class ZHAClusterAttributes extends LitElement {
</div>
${this.showHelp
? html`
<div style="color: grey; padding: 16px">
<div class="help-text">
Select an attribute to view or set its value
</div>
`
@ -152,6 +158,13 @@ export class ZHAClusterAttributes extends LitElement {
<mwc-button @click="${this._onGetZigbeeAttributeClick}"
>Get Zigbee Attribute</mwc-button
>
${this.showHelp
? html`
<div class="help-text2">
Get the value for the selected attribute
</div>
`
: ""}
<ha-call-service-button
.hass="${this.hass}"
domain="zha"
@ -165,6 +178,7 @@ export class ZHAClusterAttributes extends LitElement {
.hass="${this.hass}"
domain="zha"
service="set_zigbee_cluster_attribute"
class="help-text2"
></ha-service-description>
`
: ""}
@ -201,7 +215,7 @@ export class ZHAClusterAttributes extends LitElement {
attribute: this._attributes[this._selectedAttributeIndex].id,
manufacturer: this._manufacturerCodeOverride
? parseInt(this._manufacturerCodeOverride as string, 10)
: this.selectedNode!.manufacturer_code,
: undefined,
};
}
@ -220,7 +234,7 @@ export class ZHAClusterAttributes extends LitElement {
value: this._attributeValue,
manufacturer: this._manufacturerCodeOverride
? parseInt(this._manufacturerCodeOverride as string, 10)
: this.selectedNode!.manufacturer_code,
: undefined,
};
}
@ -312,6 +326,16 @@ export class ZHAClusterAttributes extends LitElement {
[hidden] {
display: none;
}
.help-text {
color: grey;
padding-left: 28px;
padding-right: 28px;
padding-bottom: 16px;
}
.help-text2 {
color: grey;
padding: 16px;
}
`,
];
}

View File

@ -1,15 +1,23 @@
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import "../ha-config-section";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
html,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
CSSResult,
css,
} from "lit-element";
import "@polymer/paper-card/paper-card";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import {
Cluster,
Command,
@ -18,13 +26,12 @@ import {
} from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { formatAsPaddedHex } from "./functions";
import {
ChangeEvent,
IssueCommandServiceData,
ItemSelectedEvent,
} from "./types";
import { formatAsPaddedHex } from "./functions";
export class ZHAClusterCommands extends LitElement {
public hass?: HomeAssistant;
@ -107,7 +114,7 @@ export class ZHAClusterCommands extends LitElement {
</div>
${this._showHelp
? html`
<div class="helpText">Select a command to interact with</div>
<div class="help-text">Select a command to interact with</div>
`
: ""}
${this._selectedCommandIndex !== -1
@ -135,6 +142,7 @@ export class ZHAClusterCommands extends LitElement {
.hass="${this.hass}"
domain="zha"
service="issue_zigbee_cluster_command"
class="help-text2"
></ha-service-description>
`
: ""}
@ -242,7 +250,14 @@ export class ZHAClusterCommands extends LitElement {
position: relative;
}
.helpText {
.help-text {
color: grey;
padding-left: 28px;
padding-right: 28px;
padding-bottom: 16px;
}
.help-text2 {
color: grey;
padding: 16px;
}

View File

@ -1,22 +1,27 @@
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import "../ha-config-section";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
html,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
CSSResult,
css,
} from "lit-element";
import "@polymer/paper-card/paper-card";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import { Cluster, fetchClustersForZhaNode, ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { ItemSelectedEvent } from "./types";
import { formatAsPaddedHex } from "./functions";
import { ItemSelectedEvent } from "./types";
declare global {
// for fire event
@ -90,7 +95,7 @@ export class ZHAClusters extends LitElement {
</div>
${this.showHelp
? html`
<div class="helpText">
<div class="help-text">
Select cluster to view attributes and commands
</div>
`
@ -143,9 +148,11 @@ export class ZHAClusters extends LitElement {
padding-right: 28px;
padding-bottom: 10px;
}
.helpText {
.help-text {
color: grey;
padding: 16px;
padding-left: 28px;
padding-right: 28px;
padding-bottom: 16px;
}
`,
];

View File

@ -0,0 +1,70 @@
import "../../../layouts/hass-loading-screen";
import { customElement, property } from "lit-element";
import { listenMediaQuery } from "../../../common/dom/media_query";
import {
HassRouterPage,
RouterOptions,
} from "../../../layouts/hass-router-page";
import { HomeAssistant } from "../../../types";
@customElement("zha-config-panel")
class ZHAConfigPanel extends HassRouterPage {
@property() public hass!: HomeAssistant;
@property() public _wideSidebar: boolean = false;
@property() public _wide: boolean = false;
protected routerOptions: RouterOptions = {
defaultPage: "configuration",
cacheAll: true,
preloadAll: true,
routes: {
configuration: {
tag: "ha-config-zha",
load: () =>
import(/* webpackChunkName: "zha-configuration-page" */ "./ha-config-zha"),
},
add: {
tag: "zha-add-devices-page",
load: () =>
import(/* webpackChunkName: "zha-add-devices-page" */ "./zha-add-devices-page"),
},
},
};
private _listeners: Array<() => void> = [];
public connectedCallback(): void {
super.connectedCallback();
this._listeners.push(
listenMediaQuery("(min-width: 1040px)", (matches) => {
this._wide = matches;
})
);
this._listeners.push(
listenMediaQuery("(min-width: 1296px)", (matches) => {
this._wideSidebar = matches;
})
);
}
public disconnectedCallback(): void {
super.disconnectedCallback();
while (this._listeners.length) {
this._listeners.pop()!();
}
}
protected updatePageEl(el): void {
el.route = this.routeTail;
el.hass = this.hass;
el.isWide = this.hass.dockedSidebar ? this._wideSidebar : this._wide;
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-config-panel": ZHAConfigPanel;
}
}

View File

@ -1,40 +1,123 @@
import "../../../components/buttons/ha-call-service-button";
import "../../../components/entity/state-badge";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
CSSResult,
css,
} from "lit-element";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { fireEvent } from "../../../common/dom/fire_event";
import compare from "../../../common/string/compare";
import {
AreaRegistryEntry,
fetchAreaRegistry,
} from "../../../data/area_registry";
import {
DeviceRegistryEntryMutableParams,
updateDeviceRegistryEntry,
} from "../../../data/device_registry";
import { reconfigureNode, ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { ItemSelectedEvent, NodeServiceData } from "./types";
import "../../../components/entity/state-badge";
import { ZHADevice } from "../../../data/zha";
declare global {
// for fire event
interface HASSDomEvents {
"zha-device-removed": {
device?: ZHADevice;
};
}
}
@customElement("zha-device-card")
class ZHADeviceCard extends LitElement {
@property() public hass?: HomeAssistant;
@property() public narrow?: boolean;
@property() public device?: ZHADevice;
@property() public showHelp: boolean = false;
@property() public showActions?: boolean;
@property() public isJoinPage?: boolean;
@property() private _serviceData?: NodeServiceData;
@property() private _areas: AreaRegistryEntry[] = [];
@property() private _selectedAreaIndex: number = -1;
public firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.addEventListener("hass-service-called", (ev) =>
this.serviceCalled(ev)
);
this._serviceData = {
ieee_address: this.device!.ieee,
};
fetchAreaRegistry(this.hass!).then((areas) => {
this._areas = areas.sort((a, b) => compare(a.name, b.name));
});
}
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("device")) {
this._selectedAreaIndex =
this._areas.findIndex((area) => area.area_id === this.device!.area_id) +
1;
}
super.update(changedProperties);
}
protected serviceCalled(ev): void {
// Check if this is for us
if (ev.detail.success && ev.detail.service === "remove") {
fireEvent(this, "zha-device-removed", {
device: this.device,
});
}
}
protected render(): TemplateResult | void {
return html`
<paper-card>
<paper-card heading="${this.isJoinPage ? this.device!.name : ""}">
${
this.isJoinPage
? html`
<div class="info">
<div class="model">${this.device!.model}</div>
<div class="manuf">
${this.hass!.localize(
"ui.panel.config.integrations.config_entry.manuf",
"manufacturer",
this.device!.manufacturer
)}
</div>
</div>
`
: ""
}
<div class="card-content">
<dl>
<dt class="label">IEEE:</dt>
<dd class="info">${this.device!.ieee}</dd>
<dt class="label">Quirk applied:</dt>
<dd class="info">${this.device!.quirk_applied}</dd>
<dt class="label">Quirk:</dt>
<dd class="info">${this.device!.quirk_class}</dd>
<dt>IEEE:</dt>
<dd class="zha-info">${this.device!.ieee}</dd>
${
this.device!.quirk_applied
? html`
<dt>Quirk:</dt>
<dd class="zha-info">${this.device!.quirk_class}</dd>
`
: ""
}
</dl>
</div>
@ -49,24 +132,145 @@ class ZHADeviceCard extends LitElement {
.stateObj="${this.hass!.states[entity.entity_id]}"
slot="item-icon"
></state-badge>
<paper-item-body>
<div class="name">${entity.name}</div>
<div class="secondary entity-id">${entity.entity_id}</div>
</paper-item-body>
${!this.isJoinPage
? html`
<paper-item-body>
<div class="name">${entity.name}</div>
<div class="secondary entity-id">
${entity.entity_id}
</div>
</paper-item-body>
`
: ""}
</paper-icon-item>
`
)}
</div>
<div class="editable">
<paper-input
type="string"
@change="${this._saveCustomName}"
placeholder="${this.hass!.localize(
"ui.panel.config.zha.device_card.device_name_placeholder"
)}"
></paper-input>
</div>
<div class="node-picker">
<paper-dropdown-menu
label="${this.hass!.localize(
"ui.panel.config.zha.device_card.area_picker_label"
)}"
class="flex"
>
<paper-listbox
slot="dropdown-content"
.selected="${this._selectedAreaIndex}"
@iron-select="${this._selectedAreaChanged}"
>
<paper-item>
${this.hass!.localize(
"ui.panel.config.integrations.config_entry.no_area"
)}
</paper-item>
${this._areas.map(
(entry) => html`
<paper-item area="${entry}">${entry.name}</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
</div>
${
this.showActions
? html`
<div class="card-actions">
<mwc-button @click="${this._onReconfigureNodeClick}"
>Reconfigure Device</mwc-button
>
${this.showHelp
? html`
<div class="help-text">
${this.hass!.localize(
"ui.panel.config.zha.services.reconfigure"
)}
</div>
`
: ""}
<ha-call-service-button
.hass="${this.hass}"
domain="zha"
service="remove"
.serviceData="${this._serviceData}"
>Remove Device</ha-call-service-button
>
${this.showHelp
? html`
<div class="help-text">
${this.hass!.localize(
"ui.panel.config.zha.services.remove"
)}
</div>
`
: ""}
</div>
`
: ""
}
</div>
</paper-card>
`;
}
private async _onReconfigureNodeClick(): Promise<void> {
if (this.hass) {
await reconfigureNode(this.hass, this.device!.ieee);
}
}
private async _saveCustomName(event): Promise<void> {
if (this.hass) {
const values: DeviceRegistryEntryMutableParams = {
name_by_user: event.target.value,
area_id: this.device!.area_id ? this.device!.area_id : undefined,
};
await updateDeviceRegistryEntry(
this.hass,
this.device!.device_reg_id,
values
);
this.device!.user_given_name = event.target.value;
}
}
private _openMoreInfo(ev: MouseEvent): void {
fireEvent(this, "hass-more-info", {
entityId: (ev.currentTarget as any).entity.entity_id,
});
}
private async _selectedAreaChanged(event: ItemSelectedEvent) {
if (!this.device || !this._areas) {
return;
}
this._selectedAreaIndex = event!.target!.selected;
const area = this._areas[this._selectedAreaIndex - 1]; // account for No Area
if (
(!area && !this.device.area_id) ||
(area && area.area_id === this.device.area_id)
) {
return;
}
await updateDeviceRegistryEntry(this.hass!, this.device.device_reg_id, {
area_id: area ? area.area_id : undefined,
name_by_user: this.device!.user_given_name,
});
}
static get styles(): CSSResult[] {
return [
haStyle,
@ -74,29 +278,43 @@ class ZHADeviceCard extends LitElement {
:host(:not([narrow])) .device-entities {
max-height: 225px;
overflow: auto;
display: flex;
flex-wrap: wrap;
padding: 4px;
justify-content: left;
}
paper-card {
flex: 1 0 100%;
padding-bottom: 10px;
min-width: 0;
min-width: 425px;
}
.device {
width: 30%;
}
.label {
.device .name {
font-weight: bold;
}
.device .manuf {
color: var(--secondary-text-color);
}
.extra-info {
margin-top: 8px;
}
.manuf,
.zha-info,
.entity-id {
color: var(--secondary-text-color);
}
.info {
color: var(--secondary-text-color);
font-weight: bold;
margin-left: 16px;
}
dl dt {
padding-left: 12px;
float: left;
width: 100px;
width: 50px;
text-align: left;
}
dt dd {
margin-left: 10px;
text-align: left;
}
paper-icon-item {
@ -104,6 +322,36 @@ class ZHADeviceCard extends LitElement {
padding-top: 4px;
padding-bottom: 4px;
}
.editable {
padding-left: 28px;
padding-right: 28px;
padding-bottom: 10px;
}
.help-text {
color: grey;
padding: 16px;
}
.flex {
-ms-flex: 1 1 0.000000001px;
-webkit-flex: 1;
flex: 1;
-webkit-flex-basis: 0.000000001px;
flex-basis: 0.000000001px;
}
.node-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;
}
`,
];
}
@ -114,5 +362,3 @@ declare global {
"zha-device-card": ZHADeviceCard;
}
}
customElements.define("zha-device-card", ZHADeviceCard);

View File

@ -1,19 +1,22 @@
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import "../ha-config-section";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-icon-button/paper-icon-button";
import {
css,
CSSResult,
html,
LitElement,
PropertyDeclarations,
TemplateResult,
CSSResult,
css,
} from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import { navigate } from "../../../common/navigate";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
export class ZHANetwork extends LitElement {
public hass?: HomeAssistant;
@ -30,6 +33,7 @@ export class ZHANetwork extends LitElement {
hass: {},
isWide: {},
_showHelp: {},
_joinParams: {},
};
}
@ -37,29 +41,31 @@ export class ZHANetwork extends LitElement {
return html`
<ha-config-section .isWide="${this.isWide}">
<div style="position: relative" slot="header">
<span>Network Management</span>
<paper-icon-button class="toggle-help-icon" @click="${
this._onHelpTap
}" icon="hass:help-circle"></paper-icon-button>
<span>Network Management</span>
<paper-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
></paper-icon-button>
</div>
<span slot="introduction">Commands that affect entire network</span>
<paper-card class="content">
<div class="card-actions">
<ha-call-service-button .hass="${
this.hass
}" domain="zha" service="permit">Permit</ha-call-service-button>
${
this._showHelp
? html`
<ha-service-description
.hass="${this.hass}"
domain="zha"
service="permit"
/>
`
: ""
}
<div class="card-actions">
<mwc-button @click=${this._onAddDevicesClick}>
Add Devices
</mwc-button>
${this._showHelp
? html`
<ha-service-description
.hass="${this.hass}"
domain="zha"
service="permit"
class="help-text2"
/>
`
: ""}
</div>
</paper-card>
</ha-config-section>
`;
@ -69,6 +75,10 @@ export class ZHANetwork extends LitElement {
this._showHelp = !this._showHelp;
}
private _onAddDevicesClick() {
navigate(this, "add");
}
static get styles(): CSSResult[] {
return [
haStyle,
@ -102,6 +112,11 @@ export class ZHANetwork extends LitElement {
[hidden] {
display: none;
}
.help-text2 {
color: grey;
padding: 16px;
}
`,
];
}

View File

@ -1,32 +1,30 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
CSSResult,
PropertyValues,
css,
} from "lit-element";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import "../ha-config-section";
import "./zha-clusters";
import "./zha-device-card";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import { fetchDevices, ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { ItemSelectedEvent, NodeServiceData, ChangeEvent } from "./types";
import "./zha-clusters";
import "./zha-device-card";
import {
updateDeviceRegistryEntry,
DeviceRegistryEntryMutableParams,
} from "../../../data/device_registry";
import { reconfigureNode, fetchDevices, ZHADevice } from "../../../data/zha";
import { ItemSelectedEvent, ZHADeviceRemovedEvent } from "./types";
declare global {
// for fire event
@ -37,60 +35,25 @@ declare global {
}
}
@customElement("zha-node")
export class ZHANode extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
private _showHelp: boolean;
private _selectedNodeIndex: number;
private _selectedNode?: ZHADevice;
private _serviceData?: {};
private _nodes: ZHADevice[];
private _userSelectedName?: string;
@property() public hass?: HomeAssistant;
@property() public isWide?: boolean;
@property() private _showHelp: boolean = false;
@property() private _selectedDeviceIndex: number = -1;
@property() private _selectedDevice?: ZHADevice;
@property() private _nodes: ZHADevice[] = [];
constructor() {
super();
this._showHelp = false;
this._selectedNodeIndex = -1;
this._nodes = [];
}
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
_showHelp: {},
_selectedNodeIndex: {},
_selectedNode: {},
_entities: {},
_serviceData: {},
_nodes: {},
_userSelectedName: {},
};
}
public firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this._nodes.length === 0) {
this._fetchDevices();
}
this.addEventListener("hass-service-called", (ev) =>
this.serviceCalled(ev)
);
}
protected serviceCalled(ev): void {
// Check if this is for us
if (ev.detail.success && ev.detail.service === "remove") {
this._selectedNodeIndex = -1;
this._fetchDevices();
}
public connectedCallback(): void {
super.connectedCallback();
this._fetchDevices();
}
protected render(): TemplateResult | void {
return html`
<ha-config-section .isWide="${this.isWide}">
<div class="sectionHeader" slot="header">
<span>Node Management</span>
<span>Device Management</span>
<paper-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
@ -98,8 +61,8 @@ export class ZHANode extends LitElement {
></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)
Run ZHA commands that affect a single device. Pick a device 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
@ -108,11 +71,15 @@ export class ZHANode extends LitElement {
</span>
<paper-card class="content">
<div class="node-picker">
<paper-dropdown-menu label="Nodes" class="flex">
<paper-dropdown-menu
label="Devices"
class="flex"
id="zha-device-selector"
>
<paper-listbox
slot="dropdown-content"
@iron-select="${this._selectedNodeChanged}"
.selected="${this._selectedNodeIndex}"
@iron-select="${this._selectedDeviceChanged}"
.selected="${this._selectedDeviceIndex}"
>
${this._nodes.map(
(entry) => html`
@ -128,95 +95,36 @@ export class ZHANode extends LitElement {
</div>
${this._showHelp
? html`
<div class="helpText">
Select node to view per-node options
<div class="help-text">
Select device to view per-device options
</div>
`
: ""}
${this._selectedNodeIndex !== -1
${this._selectedDeviceIndex !== -1
? html`
<zha-device-card
class="card"
.hass="${this.hass}"
.device="${this._selectedNode}"
.device="${this._selectedDevice}"
.narrow="${!this.isWide}"
.showHelp="${this._showHelp}"
.showActions="${true}"
@zha-device-removed="${this._onDeviceRemoved}"
.isJoinPage="${false}"
></zha-device-card>
`
: ""}
${this._selectedNodeIndex !== -1
? html`
<div class="input-text">
<paper-input
type="string"
.value="${this._userSelectedName}"
@value-changed="${this._onUserSelectedNameChanged}"
placeholder="User given name"
></paper-input>
</div>
`
: ""}
${this._selectedNodeIndex !== -1 ? this._renderNodeActions() : ""}
${this._selectedNode ? this._renderClusters() : ""}
${this._selectedDevice ? this._renderClusters() : ""}
</paper-card>
</ha-config-section>
`;
}
private _renderNodeActions(): TemplateResult {
return html`
<div class="card-actions">
<mwc-button @click="${this._onReconfigureNodeClick}"
>Reconfigure Node</mwc-button
>
${this._showHelp
? html`
<div class="helpText">
${this.hass!.localize(
"ui.panel.config.zha.services.reconfigure"
)}
</div>
`
: ""}
<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"
/>
`
: ""}
<mwc-button
@click="${this._onUpdateDeviceNameClick}"
.disabled="${!this._userSelectedName ||
this._userSelectedName === ""}"
>Update Name</mwc-button
>
${this._showHelp
? html`
<div class="helpText">
${this.hass!.localize(
"ui.panel.config.zha.services.updateDeviceName"
)}
</div>
`
: ""}
</div>
`;
}
private _renderClusters(): TemplateResult {
return html`
<zha-clusters
.hass="${this.hass}"
.selectedDevice="${this._selectedNode}"
.selectedDevice="${this._selectedDevice}"
.showHelp="${this._showHelp}"
></zha-clusters>
`;
@ -226,45 +134,10 @@ export class ZHANode extends LitElement {
this._showHelp = !this._showHelp;
}
private _selectedNodeChanged(event: ItemSelectedEvent): void {
this._selectedNodeIndex = event!.target!.selected;
this._selectedNode = this._nodes[this._selectedNodeIndex];
this._userSelectedName = "";
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!.ieee);
}
}
private _onUserSelectedNameChanged(value: ChangeEvent): void {
this._userSelectedName = value.detail!.value;
}
private async _onUpdateDeviceNameClick(): Promise<void> {
if (this.hass) {
const values: DeviceRegistryEntryMutableParams = {
name_by_user: this._userSelectedName,
};
await updateDeviceRegistryEntry(
this.hass,
this._selectedNode!.device_reg_id,
values
);
this._selectedNode!.user_given_name = this._userSelectedName!;
this._userSelectedName = "";
}
}
private _computeNodeServiceData(): NodeServiceData {
return {
ieee_address: this._selectedNode!.ieee,
};
private _selectedDeviceChanged(event: ItemSelectedEvent): void {
this._selectedDeviceIndex = event!.target!.selected;
this._selectedDevice = this._nodes[this._selectedDeviceIndex];
fireEvent(this, "zha-node-selected", { node: this._selectedDevice });
}
private async _fetchDevices() {
@ -273,6 +146,13 @@ export class ZHANode extends LitElement {
});
}
private _onDeviceRemoved(event: ZHADeviceRemovedEvent): void {
this._selectedDeviceIndex = -1;
this._nodes.splice(this._nodes.indexOf(event.detail!.device!), 1);
this._selectedDevice = undefined;
fireEvent(this, "zha-node-selected", { node: this._selectedDevice });
}
static get styles(): CSSResult[] {
return [
haStyle,
@ -298,13 +178,10 @@ export class ZHANode extends LitElement {
}
.help-text {
color: grey;
padding-left: 28px;
padding-right: 28px;
}
.helpText {
color: grey;
padding: 16px;
padding-bottom: 16px;
}
paper-card {
@ -355,12 +232,6 @@ export class ZHANode extends LitElement {
right: 0;
color: var(--primary-color);
}
.input-text {
padding-left: 28px;
padding-right: 28px;
padding-bottom: 10px;
}
`,
];
}
@ -371,5 +242,3 @@ declare global {
"zha-node": ZHANode;
}
}
customElements.define("zha-node", ZHANode);

View File

@ -877,7 +877,18 @@
"description": "Zigbee Home Automation network management",
"services": {
"reconfigure": "Reconfigure ZHA device (heal device). Use this if you are having issues with the device. If the device in question is a battery powered device please ensure it is awake and accepting commands when you use this service.",
"updateDeviceName": "Set a custom name for this device in the device registry."
"updateDeviceName": "Set a custom name for this device in the device registry.",
"remove": "Remove a device from the ZigBee network."
},
"device_card": {
"device_name_placeholder": "User given name",
"area_picker_label": "Area",
"update_name_button": "Update Name"
},
"add_device_page": {
"header": "Zigbee Home Automation - Add Devices",
"spinner": "Searching for ZHA Zigbee devices...",
"discovery_text": "Discovered devices will show up here. Follow the instructions for your device(s) and place the device(s) in pairing mode."
}
},
"zwave": {