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 <mail@bramkragten.nl>

* Update src/panels/config/zha/zha-group-page.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update src/panels/config/zha/zha-devices-data-table.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update src/panels/config/zha/zha-group-page.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update src/panels/config/zha/zha-group-page.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update src/panels/config/zha/zha-group-page.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* review comments

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
David F. Mulcahey 2020-01-02 09:59:18 -05:00 committed by Bram Kragten
parent 711d51c022
commit bdd18775c3
4 changed files with 329 additions and 1 deletions

View File

@ -182,3 +182,32 @@ export const fetchGroup = (
type: "zha/group",
group_id: groupId,
});
export const fetchGroupableDevices = (
hass: HomeAssistant
): Promise<ZHADevice[]> =>
hass.callWS({
type: "zha/devices/groupable",
});
export const addMembersToGroup = (
hass: HomeAssistant,
groupId: number,
membersToAdd: string[]
): Promise<ZHAGroup> =>
hass.callWS({
type: "zha/group/members/add",
group_id: groupId,
members: membersToAdd,
});
export const removeMembersFromGroup = (
hass: HomeAssistant,
groupId: number,
membersToRemove: string[]
): Promise<ZHAGroup> =>
hass.callWS({
type: "zha/group/members/remove",
group_id: groupId,
members: membersToRemove,
});

View File

@ -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`
<div @click=${this._handleClicked} style="cursor: pointer;">
${name}
</div>
`,
},
}
: {
name: {
title: "Name",
sortable: true,
filterable: true,
direction: "asc",
template: (name) => html`
<div @click=${this._handleClicked} style="cursor: pointer;">
${name}
</div>
`,
},
manufacturer: {
title: "Manufacturer",
sortable: true,
filterable: true,
},
model: {
title: "Model",
sortable: true,
filterable: true,
},
}
);
protected render(): TemplateResult {
return html`
<ha-data-table
.columns=${this._columns(this.narrow)}
.data=${this._devices(this.devices)}
.selectable=${this.selectable}
></ha-data-table>
`;
}
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;
}
}

View File

@ -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
</p>
`}
${members.length
? html`
<div class="header">
${this.hass.localize(
"ui.panel.config.zha.groups.remove_members"
)}
</div>
<zha-devices-data-table
.hass=${this.hass}
.devices=${members}
.narrow=${this.narrow}
selectable
@selection-changed=${this._handleRemoveSelectionChanged}
class="table"
>
</zha-devices-data-table>
<div class="paper-dialog-buttons">
<mwc-button
.disabled="${!this._selectedDevicesToRemove.length ||
this._processingRemove}"
@click="${this._removeMembersFromGroup}"
class="button"
>
<paper-spinner
?active="${this._processingRemove}"
alt=${this.hass.localize(
"ui.panel.config.zha.groups.removing_members"
)}
></paper-spinner>
${this.hass!.localize(
"ui.panel.config.zha.groups.remove_members"
)}</mwc-button
>
</div>
`
: html``}
<div class="header">
${this.hass.localize("ui.panel.config.zha.groups.add_members")}
</div>
<zha-devices-data-table
.hass=${this.hass}
.devices=${this._filteredDevices}
.narrow=${this.narrow}
selectable
@selection-changed=${this._handleAddSelectionChanged}
class="table"
>
</zha-devices-data-table>
<div class="paper-dialog-buttons">
<mwc-button
.disabled="${!this._selectedDevicesToAdd.length ||
this._processingAdd}"
@click="${this._addMembersToGroup}"
class="button"
>
<paper-spinner
?active="${this._processingAdd}"
alt=${this.hass.localize(
"ui.panel.config.zha.groups.adding_members"
)}
></paper-spinner>
${this.hass!.localize(
"ui.panel.config.zha.groups.add_members"
)}</mwc-button
>
</div>
</ha-config-section>
</hass-subpage>
`;
@ -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<void> {
this._processingAdd = true;
this.group = await addMembersToGroup(
this.hass,
this.groupId,
this._selectedDevicesToAdd
);
this._filterDevices();
this._selectedDevicesToAdd = [];
this._processingAdd = false;
}
private async _removeMembersFromGroup(): Promise<void> {
this._processingRemove = true;
this.group = await removeMembersFromGroup(
this.hass,
this.groupId,
this._selectedDevicesToRemove
);
this._filterDevices();
this._selectedDevicesToRemove = [];
this._processingRemove = false;
}
private async _deleteGroup(): Promise<void> {
@ -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);
}
`,
];
}

View File

@ -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": {