Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/data/zha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
110 changes: 110 additions & 0 deletions src/panels/config/zha/zha-devices-data-table.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
185 changes: 185 additions & 0 deletions src/panels/config/zha/zha-group-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(
Expand All @@ -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) {
Expand Down Expand Up @@ -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>
`;
Expand All @@ -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> {
Expand All @@ -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);
}
`,
];
}
Expand Down
6 changes: 5 additions & 1 deletion src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down