From bdd18775c37e2685bb9dbc36cc0fa0c3b0863c5b Mon Sep 17 00:00:00 2001
From: "David F. Mulcahey"
Date: Thu, 2 Jan 2020 09:59:18 -0500
Subject: [PATCH] Add group editing to the ZHA config panel (#4382)
* add group editing
* Update src/panels/config/zha/zha-devices-data-table.ts
Co-Authored-By: Bram Kragten
* Update src/panels/config/zha/zha-group-page.ts
Co-Authored-By: Bram Kragten
* Update src/panels/config/zha/zha-devices-data-table.ts
Co-Authored-By: Bram Kragten
* Update src/panels/config/zha/zha-group-page.ts
Co-Authored-By: Bram Kragten
* Update src/panels/config/zha/zha-group-page.ts
Co-Authored-By: Bram Kragten
* Update src/panels/config/zha/zha-group-page.ts
Co-Authored-By: Bram Kragten
* review comments
Co-authored-by: Bram Kragten
---
src/data/zha.ts | 29 +++
.../config/zha/zha-devices-data-table.ts | 110 +++++++++++
src/panels/config/zha/zha-group-page.ts | 185 ++++++++++++++++++
src/translations/en.json | 6 +-
4 files changed, 329 insertions(+), 1 deletion(-)
create mode 100644 src/panels/config/zha/zha-devices-data-table.ts
diff --git a/src/data/zha.ts b/src/data/zha.ts
index 4d8306f332..fb227d52fd 100644
--- a/src/data/zha.ts
+++ b/src/data/zha.ts
@@ -182,3 +182,32 @@ export const fetchGroup = (
type: "zha/group",
group_id: groupId,
});
+
+export const fetchGroupableDevices = (
+ hass: HomeAssistant
+): Promise =>
+ hass.callWS({
+ type: "zha/devices/groupable",
+ });
+
+export const addMembersToGroup = (
+ hass: HomeAssistant,
+ groupId: number,
+ membersToAdd: string[]
+): Promise =>
+ hass.callWS({
+ type: "zha/group/members/add",
+ group_id: groupId,
+ members: membersToAdd,
+ });
+
+export const removeMembersFromGroup = (
+ hass: HomeAssistant,
+ groupId: number,
+ membersToRemove: string[]
+): Promise =>
+ hass.callWS({
+ type: "zha/group/members/remove",
+ group_id: groupId,
+ members: membersToRemove,
+ });
diff --git a/src/panels/config/zha/zha-devices-data-table.ts b/src/panels/config/zha/zha-devices-data-table.ts
new file mode 100644
index 0000000000..c647f3c430
--- /dev/null
+++ b/src/panels/config/zha/zha-devices-data-table.ts
@@ -0,0 +1,110 @@
+import "../../../components/data-table/ha-data-table";
+import "../../../components/entity/ha-state-icon";
+
+import memoizeOne from "memoize-one";
+
+import {
+ LitElement,
+ html,
+ TemplateResult,
+ property,
+ customElement,
+} from "lit-element";
+import { HomeAssistant } from "../../../types";
+// tslint:disable-next-line
+import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
+// tslint:disable-next-line
+import { ZHADevice } from "../../../data/zha";
+import { showZHADeviceInfoDialog } from "../../../dialogs/zha-device-info-dialog/show-dialog-zha-device-info";
+
+export interface DeviceRowData extends ZHADevice {
+ device?: DeviceRowData;
+}
+
+@customElement("zha-devices-data-table")
+export class ZHADevicesDataTable extends LitElement {
+ @property() public hass!: HomeAssistant;
+ @property() public narrow = false;
+ @property({ type: Boolean }) public selectable = false;
+ @property() public devices: ZHADevice[] = [];
+
+ private _devices = memoizeOne((devices: ZHADevice[]) => {
+ let outputDevices: DeviceRowData[] = devices;
+
+ outputDevices = outputDevices.map((device) => {
+ return {
+ ...device,
+ name: device.user_given_name || device.name,
+ model: device.model,
+ manufacturer: device.manufacturer,
+ id: device.ieee,
+ };
+ });
+
+ return outputDevices;
+ });
+
+ private _columns = memoizeOne(
+ (narrow: boolean): DataTableColumnContainer =>
+ narrow
+ ? {
+ name: {
+ title: "Devices",
+ sortable: true,
+ filterable: true,
+ direction: "asc",
+ template: (name) => html`
+
+ ${name}
+
+ `,
+ },
+ }
+ : {
+ name: {
+ title: "Name",
+ sortable: true,
+ filterable: true,
+ direction: "asc",
+ template: (name) => html`
+
+ ${name}
+
+ `,
+ },
+ manufacturer: {
+ title: "Manufacturer",
+ sortable: true,
+ filterable: true,
+ },
+ model: {
+ title: "Model",
+ sortable: true,
+ filterable: true,
+ },
+ }
+ );
+
+ protected render(): TemplateResult {
+ return html`
+
+ `;
+ }
+
+ private async _handleClicked(ev: CustomEvent) {
+ const ieee = (ev.target as HTMLElement)
+ .closest("tr")!
+ .getAttribute("data-row-id")!;
+ showZHADeviceInfoDialog(this, { ieee });
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "zha-devices-data-table": ZHADevicesDataTable;
+ }
+}
diff --git a/src/panels/config/zha/zha-group-page.ts b/src/panels/config/zha/zha-group-page.ts
index 005b67acf8..8aec748c87 100644
--- a/src/panels/config/zha/zha-group-page.ts
+++ b/src/panels/config/zha/zha-group-page.ts
@@ -19,11 +19,18 @@ import {
ZHAGroup,
fetchGroup,
removeGroups,
+ fetchGroupableDevices,
+ addMembersToGroup,
+ removeMembersFromGroup,
} from "../../../data/zha";
import { formatAsPaddedHex } from "./functions";
import "./zha-device-card";
+import "./zha-devices-data-table";
import { navigate } from "../../../common/navigate";
import "@polymer/paper-icon-button/paper-icon-button";
+import "@polymer/paper-spinner/paper-spinner";
+import "@material/mwc-button";
+import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table";
@customElement("zha-group-page")
export class ZHAGroupPage extends LitElement {
@@ -32,6 +39,13 @@ export class ZHAGroupPage extends LitElement {
@property() public groupId!: number;
@property() public narrow!: boolean;
@property() public isWide!: boolean;
+ @property() public devices: ZHADevice[] = [];
+ @property() private _processingAdd: boolean = false;
+ @property() private _processingRemove: boolean = false;
+ @property() private _filteredDevices: ZHADevice[] = [];
+ @property() private _selectedDevicesToAdd: string[] = [];
+ @property() private _selectedDevicesToRemove: string[] = [];
+
private _firstUpdatedCalled: boolean = false;
private _members = memoizeOne(
@@ -45,6 +59,16 @@ export class ZHAGroupPage extends LitElement {
}
}
+ public disconnectedCallback(): void {
+ super.disconnectedCallback();
+ this._processingAdd = false;
+ this._processingRemove = false;
+ this._selectedDevicesToRemove = [];
+ this._selectedDevicesToAdd = [];
+ this.devices = [];
+ this._filteredDevices = [];
+ }
+
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.hass) {
@@ -105,6 +129,77 @@ export class ZHAGroupPage extends LitElement {
This group has no members
`}
+ ${members.length
+ ? html`
+
+
+
+
+
+
+ `
+ : html``}
+
+
+
+
+
+
+
`;
@@ -114,6 +209,68 @@ export class ZHAGroupPage extends LitElement {
if (this.groupId !== null && this.groupId !== undefined) {
this.group = await fetchGroup(this.hass!, this.groupId);
}
+ this.devices = await fetchGroupableDevices(this.hass!);
+ // filter the groupable devices so we only show devices that aren't already in the group
+ this._filterDevices();
+ }
+
+ private _filterDevices() {
+ // filter the groupable devices so we only show devices that aren't already in the group
+ this._filteredDevices = this.devices.filter((device) => {
+ return !this.group!.members.some((member) => member.ieee === device.ieee);
+ });
+ }
+
+ private _handleAddSelectionChanged(ev: CustomEvent): void {
+ const changedSelection = ev.detail as SelectionChangedEvent;
+ const entity = changedSelection.id;
+ if (changedSelection.selected) {
+ this._selectedDevicesToAdd.push(entity);
+ } else {
+ const index = this._selectedDevicesToAdd.indexOf(entity);
+ if (index !== -1) {
+ this._selectedDevicesToAdd.splice(index, 1);
+ }
+ }
+ this._selectedDevicesToAdd = [...this._selectedDevicesToAdd];
+ }
+
+ private _handleRemoveSelectionChanged(ev: CustomEvent): void {
+ const changedSelection = ev.detail as SelectionChangedEvent;
+ const entity = changedSelection.id;
+ if (changedSelection.selected) {
+ this._selectedDevicesToRemove.push(entity);
+ } else {
+ const index = this._selectedDevicesToRemove.indexOf(entity);
+ if (index !== -1) {
+ this._selectedDevicesToRemove.splice(index, 1);
+ }
+ }
+ this._selectedDevicesToRemove = [...this._selectedDevicesToRemove];
+ }
+
+ private async _addMembersToGroup(): Promise {
+ this._processingAdd = true;
+ this.group = await addMembersToGroup(
+ this.hass,
+ this.groupId,
+ this._selectedDevicesToAdd
+ );
+ this._filterDevices();
+ this._selectedDevicesToAdd = [];
+ this._processingAdd = false;
+ }
+
+ private async _removeMembersFromGroup(): Promise {
+ this._processingRemove = true;
+ this.group = await removeMembersFromGroup(
+ this.hass,
+ this.groupId,
+ this._selectedDevicesToRemove
+ );
+ this._filterDevices();
+ this._selectedDevicesToRemove = [];
+ this._processingRemove = false;
}
private async _deleteGroup(): Promise {
@@ -139,6 +296,34 @@ export class ZHAGroupPage extends LitElement {
ha-config-section *:last-child {
padding-bottom: 24px;
}
+
+ .button {
+ float: right;
+ }
+
+ .table {
+ height: 200px;
+ overflow: auto;
+ }
+
+ mwc-button paper-spinner {
+ width: 14px;
+ height: 14px;
+ margin-right: 20px;
+ }
+ paper-spinner {
+ display: none;
+ }
+ paper-spinner[active] {
+ display: block;
+ }
+ .paper-dialog-buttons {
+ align-items: flex-end;
+ padding: 8px;
+ }
+ .paper-dialog-buttons .warning {
+ --mdc-theme-primary: var(--google-red-500);
+ }
`,
];
}
diff --git a/src/translations/en.json b/src/translations/en.json
index c67219b471..b718531ec9 100755
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -1450,7 +1450,11 @@
"removing_groups": "Removing Groups",
"group_info": "Group Information",
"group_details": "Here are all the details for the selected Zigbee group.",
- "group_not_found": "Group not found!"
+ "group_not_found": "Group not found!",
+ "add_members": "Add Members",
+ "remove_members": "Remove Members",
+ "adding_members": "Adding Members",
+ "removing_members": "Removing Members"
}
},
"zwave": {