mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-01 21:47:46 +00:00
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:
parent
e96c9daad6
commit
6d43c9e86a
105
src/data/zha.ts
Normal file
105
src/data/zha.ts
Normal 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",
|
||||||
|
});
|
@ -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-header/app-header";
|
||||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
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 "@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 "../../../layouts/ha-app-layout";
|
||||||
import "../../../resources/ha-style";
|
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-network";
|
||||||
|
import "./zha-node";
|
||||||
|
|
||||||
export class HaConfigZha extends LitElement {
|
export class HaConfigZha extends LitElement {
|
||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
public isWide?: boolean;
|
public isWide?: boolean;
|
||||||
private _haStyle?: DocumentFragment;
|
private _haStyle?: DocumentFragment;
|
||||||
private _ironFlex?: DocumentFragment;
|
private _ironFlex?: DocumentFragment;
|
||||||
|
private _selectedNode?: HassEntity;
|
||||||
|
private _selectedCluster?: Cluster;
|
||||||
|
private _selectedEntity?: HassEntity;
|
||||||
|
|
||||||
static get properties(): PropertyDeclarations {
|
static get properties(): PropertyDeclarations {
|
||||||
return {
|
return {
|
||||||
hass: {},
|
hass: {},
|
||||||
isWide: {},
|
isWide: {},
|
||||||
|
_selectedCluster: {},
|
||||||
|
_selectedEntity: {},
|
||||||
|
_selectedNode: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
${this.renderStyle()}
|
${this.renderStyle()}
|
||||||
<ha-app-layout has-scrolling-region="">
|
<ha-app-layout>
|
||||||
<app-header slot="header" fixed="">
|
<app-header slot="header">
|
||||||
<app-toolbar>
|
<app-toolbar>
|
||||||
<paper-icon-button
|
<paper-icon-button
|
||||||
icon="hass:arrow-left"
|
icon="hass:arrow-left"
|
||||||
@click="${this._onBackTapped}"
|
@click="${this._onBackTapped}"
|
||||||
></paper-icon-button>
|
></paper-icon-button>
|
||||||
|
<div main-title>Zigbee Home Automation</div>
|
||||||
</app-toolbar>
|
</app-toolbar>
|
||||||
</app-header>
|
</app-header>
|
||||||
|
|
||||||
<zha-network
|
<zha-network
|
||||||
id="zha-network"
|
.isWide="${this.isWide}"
|
||||||
.is-wide="${this.isWide}"
|
|
||||||
.hass="${this.hass}"
|
.hass="${this.hass}"
|
||||||
></zha-network>
|
></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>
|
</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 {
|
private renderStyle(): TemplateResult {
|
||||||
if (!this._haStyle) {
|
if (!this._haStyle) {
|
||||||
this._haStyle = document.importNode(
|
this._haStyle = document.importNode(
|
||||||
|
50
src/panels/config/zha/types.ts
Normal file
50
src/panels/config/zha/types.ts
Normal 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;
|
||||||
|
}
|
329
src/panels/config/zha/zha-cluster-attributes.ts
Normal file
329
src/panels/config/zha/zha-cluster-attributes.ts
Normal 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);
|
282
src/panels/config/zha/zha-cluster-commands.ts
Normal file
282
src/panels/config/zha/zha-cluster-commands.ts
Normal 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);
|
165
src/panels/config/zha/zha-clusters.ts
Normal file
165
src/panels/config/zha/zha-clusters.ts
Normal 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);
|
177
src/panels/config/zha/zha-entities.ts
Normal file
177
src/panels/config/zha/zha-entities.ts
Normal 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);
|
@ -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 "@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/buttons/ha-call-service-button";
|
||||||
import "../../../components/ha-service-description";
|
import "../../../components/ha-service-description";
|
||||||
import "../ha-config-section";
|
|
||||||
|
|
||||||
import { HomeAssistant } from "../../../types";
|
|
||||||
import "../../../resources/ha-style";
|
import "../../../resources/ha-style";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import "../ha-config-section";
|
||||||
|
|
||||||
export class ZHANetwork extends LitElement {
|
export class ZHANetwork extends LitElement {
|
||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
public isWide?: boolean;
|
public isWide?: boolean;
|
||||||
public showDescription: boolean;
|
private _showHelp: boolean;
|
||||||
private _haStyle?: DocumentFragment;
|
private _haStyle?: DocumentFragment;
|
||||||
private _ironFlex?: DocumentFragment;
|
private _ironFlex?: DocumentFragment;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.showDescription = false;
|
this._showHelp = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get properties(): PropertyDeclarations {
|
static get properties(): PropertyDeclarations {
|
||||||
return {
|
return {
|
||||||
hass: {},
|
hass: {},
|
||||||
isWide: {},
|
isWide: {},
|
||||||
showDescription: {},
|
_showHelp: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
${this.renderStyle()}
|
${this.renderStyle()}
|
||||||
<ha-config-section .is-wide="${this.isWide}">
|
<ha-config-section .isWide="${this.isWide}">
|
||||||
<div style="position: relative" slot="header">
|
<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="${
|
<paper-icon-button class="toggle-help-icon" @click="${
|
||||||
this._onHelpTap
|
this._onHelpTap
|
||||||
}" icon="hass:help-circle"></paper-icon-button>
|
}" icon="hass:help-circle"></paper-icon-button>
|
||||||
@ -49,7 +48,7 @@ export class ZHANetwork extends LitElement {
|
|||||||
this.hass
|
this.hass
|
||||||
}" domain="zha" service="permit">Permit</ha-call-service-button>
|
}" domain="zha" service="permit">Permit</ha-call-service-button>
|
||||||
${
|
${
|
||||||
this.showDescription
|
this._showHelp
|
||||||
? html`
|
? html`
|
||||||
<ha-service-description
|
<ha-service-description
|
||||||
.hass="${this.hass}"
|
.hass="${this.hass}"
|
||||||
@ -65,7 +64,7 @@ export class ZHANetwork extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _onHelpTap(): void {
|
private _onHelpTap(): void {
|
||||||
this.showDescription = !this.showDescription;
|
this._showHelp = !this._showHelp;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderStyle(): TemplateResult {
|
private renderStyle(): TemplateResult {
|
||||||
|
321
src/panels/config/zha/zha-node.ts
Normal file
321
src/panels/config/zha/zha-node.ts
Normal 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);
|
Loading…
x
Reference in New Issue
Block a user