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` +
+ ${this.hass.localize( + "ui.panel.config.zha.groups.remove_members" + )} +
+ + + + +
+ + + ${this.hass!.localize( + "ui.panel.config.zha.groups.remove_members" + )} +
+ ` + : html``} + +
+ ${this.hass.localize("ui.panel.config.zha.groups.add_members")} +
+ + + + +
+ + + ${this.hass!.localize( + "ui.panel.config.zha.groups.add_members" + )} +
`; @@ -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": {