diff --git a/src/data/zha.ts b/src/data/zha.ts index 4ae16c680aad..362007f94771 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -24,6 +24,12 @@ export interface ZHADevice { area_id?: string; } +export interface ZHAGroup { + name: string; + group_id: number; + members: ZHADevice[]; +} + export interface Attribute { name: string; id: number; @@ -80,6 +86,69 @@ export const fetchDevices = (hass: HomeAssistant): Promise => type: "zha/devices", }); +export const fetchGroupableDevices = ( + hass: HomeAssistant +): Promise => + hass.callWS({ + type: "zha/devices/groupable", + }); + +export const fetchGroups = (hass: HomeAssistant): Promise => + hass.callWS({ + type: "zha/groups", + }); + +export const removeGroups = ( + hass: HomeAssistant, + groupIdsToRemove: number[] +): Promise => + hass.callWS({ + type: "zha/group/remove", + group_ids: groupIdsToRemove, + }); + +export const fetchGroup = ( + hass: HomeAssistant, + groupId: number +): Promise => + hass.callWS({ + type: "zha/group", + group_id: groupId, + }); + +export const addGroup = ( + hass: HomeAssistant, + groupName: string, + membersToAdd?: string[] +): Promise => + hass.callWS({ + type: "zha/group/add", + group_name: groupName, + members: membersToAdd, + }); + +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, + }); + export const fetchZHADevice = ( hass: HomeAssistant, ieeeAddress: string diff --git a/src/panels/config/zha/functions.ts b/src/panels/config/zha/functions.ts index 72ce4d8bdff8..1e7a2858de34 100644 --- a/src/panels/config/zha/functions.ts +++ b/src/panels/config/zha/functions.ts @@ -1,4 +1,4 @@ -import { ZHADevice } from "../../../data/zha"; +import { ZHADevice, ZHAGroup } from "../../../data/zha"; export const formatAsPaddedHex = (value: string | number): string => { let hex = value; @@ -13,3 +13,9 @@ export const sortZHADevices = (a: ZHADevice, b: ZHADevice): number => { const nameb = b.user_given_name ? b.user_given_name : b.name; return nameA.localeCompare(nameb); }; + +export const sortZHAGroups = (a: ZHAGroup, b: ZHAGroup): number => { + const nameA = a.name; + const nameb = b.name; + return nameA.localeCompare(nameb); +}; diff --git a/src/panels/config/zha/ha-config-zha.ts b/src/panels/config/zha/ha-config-zha.ts index 47db6ae3dff1..856bad324430 100755 --- a/src/panels/config/zha/ha-config-zha.ts +++ b/src/panels/config/zha/ha-config-zha.ts @@ -5,6 +5,7 @@ import "./zha-cluster-attributes"; import "./zha-cluster-commands"; import "./zha-network"; import "./zha-node"; +import "./zha-groups-tile"; import "@polymer/paper-icon-button/paper-icon-button"; import { @@ -45,12 +46,18 @@ export class HaConfigZha extends LitElement { .hass="${this.hass}" > + + + ${this._selectedCluster ? html` + + + ${this.hass.localize( + "ui.panel.config.zha.common.create_group_details" + )} + + + +
+ ${this.hass.localize("ui.panel.config.zha.common.add_members")} +
+ + + + +
+ + + ${this.hass!.localize( + "ui.panel.config.zha.common.create" + )} +
+
+ + `; + } + + private async _fetchData() { + this.devices = await fetchGroupableDevices(this.hass!); + } + + 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); + } + } + } + + private async _createGroup(): Promise { + this._processingAdd = true; + const group: ZHAGroup = await addGroup( + this.hass, + this._groupName, + this._selectedDevicesToAdd + ); + this._selectedDevicesToAdd = []; + this._canAdd = false; + this._processingAdd = false; + this._groupName = ""; + navigate(this, `/config/zha/group/${group.group_id}`, true); + } + + private _handleNameChange(ev: PolymerChangedEvent) { + const target = ev.currentTarget as PaperInputElement; + if (target.value) { + this._groupName = target.value; + if (target.value.length > 0) { + this._canAdd = true; + } + } + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + .header { + font-family: var(--paper-font-display1_-_font-family); + -webkit-font-smoothing: var( + --paper-font-display1_-_-webkit-font-smoothing + ); + font-size: var(--paper-font-display1_-_font-size); + font-weight: var(--paper-font-display1_-_font-weight); + letter-spacing: var(--paper-font-display1_-_letter-spacing); + line-height: var(--paper-font-display1_-_line-height); + opacity: var(--dark-primary-opacity); + } + + .button { + float: right; + } + + .table { + height: 400px; + overflow: auto; + } + + ha-config-section *:last-child { + padding-bottom: 24px; + } + mwc-button paper-spinner { + width: 14px; + height: 14px; + margin-right: 20px; + } + paper-spinner { + display: none; + } + paper-spinner[active] { + display: block; + } + `, + ]; + } +} diff --git a/src/panels/config/zha/zha-config-panel.ts b/src/panels/config/zha/zha-config-panel.ts index 59aeb7db87c6..f0c0124fc239 100644 --- a/src/panels/config/zha/zha-config-panel.ts +++ b/src/panels/config/zha/zha-config-panel.ts @@ -12,6 +12,7 @@ import { HomeAssistant } from "../../../types"; class ZHAConfigPanel extends HassRouterPage { @property() public hass!: HomeAssistant; @property() public isWide!: boolean; + @property() public narrow!: boolean; protected routerOptions: RouterOptions = { defaultPage: "configuration", @@ -32,6 +33,25 @@ class ZHAConfigPanel extends HassRouterPage { /* webpackChunkName: "zha-add-devices-page" */ "./zha-add-devices-page" ), }, + groups: { + tag: "zha-groups-dashboard", + load: () => + import( + /* webpackChunkName: "zha-groups-dashboard" */ "./zha-groups-dashboard" + ), + }, + group: { + tag: "zha-group-page", + load: () => + import(/* webpackChunkName: "zha-group-page" */ "./zha-group-page"), + }, + "group-add": { + tag: "zha-add-group-page", + load: () => + import( + /* webpackChunkName: "zha-add-group-page" */ "./zha-add-group-page" + ), + }, }, }; @@ -39,6 +59,10 @@ class ZHAConfigPanel extends HassRouterPage { el.route = this.routeTail; el.hass = this.hass; el.isWide = this.isWide; + el.narrow = this.narrow; + if (this._currentPage === "group") { + el.groupId = this.routeTail.path.substr(1); + } } } 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 000000000000..a26704aee678 --- /dev/null +++ b/src/panels/config/zha/zha-devices-data-table.ts @@ -0,0 +1,112 @@ +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() 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.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, + direction: "asc", + }, + model: { + title: "Model", + sortable: true, + filterable: true, + direction: "asc", + }, + } + ); + + 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 new file mode 100644 index 000000000000..12fd064171d6 --- /dev/null +++ b/src/panels/config/zha/zha-group-page.ts @@ -0,0 +1,297 @@ +import { + property, + LitElement, + html, + customElement, + css, + CSSResult, +} from "lit-element"; + +import memoizeOne from "memoize-one"; + +import "../../../layouts/hass-subpage"; +import "../../../layouts/hass-error-screen"; +import "../ha-config-section"; +import { HomeAssistant } from "../../../types"; +import { haStyleDialog } from "../../../resources/styles"; +import { + ZHADevice, + ZHAGroup, + fetchGroup, + fetchGroupableDevices, + addMembersToGroup, + removeMembersFromGroup, + removeGroups, +} from "../../../data/zha"; +import { formatAsPaddedHex } from "./functions"; +import "./zha-devices-data-table"; +import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table"; +import { navigate } from "../../../common/navigate"; + +@customElement("zha-group-page") +export class ZHAGroupPage extends LitElement { + @property() public hass!: HomeAssistant; + @property() public group!: ZHAGroup; + @property() public devices: ZHADevice[] = []; + @property() public groupId!: number; + @property() public narrow!: boolean; + @property() private _canAdd: boolean = false; + @property() private _processingAdd: boolean = false; + @property() private _canRemove: boolean = false; + @property() private _processingRemove: boolean = false; + + private _selectedDevicesToAdd: string[] = []; + private _selectedDevicesToRemove: string[] = []; + private _filteredDevices: ZHADevice[] = []; + + private _members = memoizeOne( + (group: ZHAGroup): ZHADevice[] => group.members + ); + + public connectedCallback(): void { + super.connectedCallback(); + this._fetchData(); + } + + protected render() { + if (!this.group) { + return html` + + `; + } + + const members = this._members(this.group); + + return html` + + + +
+ ${this.hass.localize("ui.panel.config.zha.common.group_info")} +
+ + ${this.hass.localize("ui.panel.config.zha.common.group_details")} + + Name: ${this.group.name} + + Group Id: ${formatAsPaddedHex(this.group.group_id)} + +
+ ${this.hass.localize("ui.panel.config.zha.common.members")} +
+ + ${members.length + ? members.map( + (member) => html` + + ` + ) + : html` + + This group has no members + + `} + ${members.length + ? html` +
+ ${this.hass.localize( + "ui.panel.config.zha.common.remove_members" + )} +
+ + + + +
+ + + ${this.hass!.localize( + "ui.panel.config.zha.common.remove_members" + )} +
+ ` + : html``} + +
+ ${this.hass.localize("ui.panel.config.zha.common.add_members")} +
+ + + + +
+ + + ${this.hass!.localize( + "ui.panel.config.zha.common.add_members" + )} +
+
+
+ `; + } + + private async _fetchData() { + 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.filter((member) => member.ieee === device.ieee) + .length > 0 + ); + }); + } + + 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._canAdd = this._selectedDevicesToAdd.length > 0; + } + + 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._canRemove = this._selectedDevicesToRemove.length > 0; + } + + private async _addMembersToGroup(): Promise { + this._processingAdd = true; + this.group = await addMembersToGroup( + this.hass, + this.groupId, + this._selectedDevicesToAdd + ); + this._filterDevices(); + this._selectedDevicesToAdd = []; + this._canAdd = false; + 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._canRemove = false; + this._processingRemove = false; + } + + private async _deleteGroup(): Promise { + await removeGroups(this.hass, [this.groupId]); + navigate(this, `/config/zha/groups`, true); + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + .header { + font-family: var(--paper-font-display1_-_font-family); + -webkit-font-smoothing: var( + --paper-font-display1_-_-webkit-font-smoothing + ); + font-size: var(--paper-font-display1_-_font-size); + font-weight: var(--paper-font-display1_-_font-weight); + letter-spacing: var(--paper-font-display1_-_letter-spacing); + line-height: var(--paper-font-display1_-_line-height); + opacity: var(--dark-primary-opacity); + } + + .button { + float: right; + } + + .table { + height: 200px; + overflow: auto; + } + + ha-config-section *:last-child { + padding-bottom: 24px; + } + mwc-button paper-spinner { + width: 14px; + height: 14px; + margin-right: 20px; + } + paper-spinner { + display: none; + } + paper-spinner[active] { + display: block; + } + `, + ]; + } +} diff --git a/src/panels/config/zha/zha-groups-dashboard.ts b/src/panels/config/zha/zha-groups-dashboard.ts new file mode 100644 index 000000000000..cad3ca98f1c7 --- /dev/null +++ b/src/panels/config/zha/zha-groups-dashboard.ts @@ -0,0 +1,146 @@ +import "../../../layouts/hass-subpage"; +import "./zha-groups-data-table"; + +import { + LitElement, + html, + TemplateResult, + property, + customElement, + CSSResult, + css, +} from "lit-element"; +import { HomeAssistant } from "../../../types"; +import { haStyleDialog } from "../../../resources/styles"; +import { ZHAGroup, fetchGroups, removeGroups } from "../../../data/zha"; +import { sortZHAGroups } from "./functions"; +import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table"; +import { navigate } from "../../../common/navigate"; + +@customElement("zha-groups-dashboard") +export class ZHAGroupsDashboard extends LitElement { + @property() public hass!: HomeAssistant; + @property() public narrow = false; + @property() public _groups!: ZHAGroup[]; + @property() private _canRemove: boolean = false; + @property() private _processingRemove: boolean = false; + + private _selectedGroupsToRemove: number[] = []; + + public connectedCallback(): void { + super.connectedCallback(); + this._fetchGroups(); + } + + protected render(): TemplateResult { + return html` + + + +
+ +
+ +
+ + + ${this.hass!.localize( + "ui.panel.config.zha.common.remove_groups" + )} +
+
+ `; + } + + private async _fetchGroups() { + this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups); + } + + private _handleRemoveSelectionChanged(ev: CustomEvent): void { + const changedSelection = ev.detail as SelectionChangedEvent; + const groupId = Number(changedSelection.id); + if (changedSelection.selected) { + this._selectedGroupsToRemove.push(groupId); + } else { + const index = this._selectedGroupsToRemove.indexOf(groupId); + if (index !== -1) { + this._selectedGroupsToRemove.splice(index, 1); + } + } + this._canRemove = this._selectedGroupsToRemove.length > 0; + } + + private async _removeGroup(): Promise { + this._processingRemove = true; + this._groups = await removeGroups(this.hass, this._selectedGroupsToRemove); + this._selectedGroupsToRemove = []; + this._canRemove = false; + this._processingRemove = false; + } + + private async _addGroup(): Promise { + navigate(this, `/config/zha/group-add`); + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + .content { + padding: 4px; + } + zha-groups-data-table { + width: 100%; + } + .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; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-groups-dashboard": ZHAGroupsDashboard; + } +} diff --git a/src/panels/config/zha/zha-groups-data-table.ts b/src/panels/config/zha/zha-groups-data-table.ts new file mode 100644 index 000000000000..dee9b0833d95 --- /dev/null +++ b/src/panels/config/zha/zha-groups-data-table.ts @@ -0,0 +1,124 @@ +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 { navigate } from "../../../common/navigate"; +import { ZHAGroup, ZHADevice } from "../../../data/zha"; +import { formatAsPaddedHex } from "./functions"; + +export interface GroupRowData extends ZHAGroup { + group?: GroupRowData; + id?: number; +} + +@customElement("zha-groups-data-table") +export class ZHAGroupsDataTable extends LitElement { + @property() public hass!: HomeAssistant; + @property() public narrow = false; + @property() public groups: ZHAGroup[] = []; + @property() public selectable = false; + + private _groups = memoizeOne((groups: ZHAGroup[]) => { + let outputGroups: GroupRowData[] = groups || []; + + outputGroups = outputGroups.map((group) => { + return { + ...group, + name: group.name, + group_id: group.group_id, + members: group.members, + id: group.group_id, + }; + }); + + return outputGroups; + }); + + private _columns = memoizeOne( + (narrow: boolean): DataTableColumnContainer => + narrow + ? { + name: { + title: "Group", + sortable: true, + filterable: true, + direction: "asc", + template: (name) => html` +
+ ${name} +
+ `, + }, + } + : { + name: { + title: this.hass.localize("ui.panel.config.zha.common.groups"), + sortable: true, + filterable: true, + direction: "asc", + template: (name) => html` +
+ ${name} +
+ `, + }, + group_id: { + title: this.hass.localize("ui.panel.config.zha.common.group_id"), + template: (groupId: number) => { + return html` + ${formatAsPaddedHex(groupId)} + `; + }, + sortable: true, + filterable: true, + direction: "asc", + }, + members: { + title: this.hass.localize("ui.panel.config.zha.common.members"), + template: (members: ZHADevice[]) => { + return html` + ${members.length} + `; + }, + sortable: true, + filterable: true, + direction: "asc", + }, + } + ); + + protected render(): TemplateResult { + return html` + + `; + } + + private _handleRowClicked(ev: CustomEvent) { + const groupId = (ev.target as HTMLElement) + .closest("tr")! + .getAttribute("data-row-id")!; + navigate(this, `/config/zha/group/${groupId}`); + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-groups-data-table": ZHAGroupsDataTable; + } +} diff --git a/src/panels/config/zha/zha-groups-tile.ts b/src/panels/config/zha/zha-groups-tile.ts new file mode 100644 index 000000000000..951df82b4abb --- /dev/null +++ b/src/panels/config/zha/zha-groups-tile.ts @@ -0,0 +1,112 @@ +import "../../../components/ha-card"; +import "../ha-config-section"; +import "@material/mwc-button"; +import "@polymer/paper-icon-button/paper-icon-button"; + +import { + css, + CSSResult, + html, + LitElement, + TemplateResult, + property, + customElement, +} from "lit-element"; + +import { navigate } from "../../../common/navigate"; +import { haStyle } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; + +@customElement("zha-groups-tile") +export class ZHAGroupsTile extends LitElement { + @property() public hass?: HomeAssistant; + @property() public isWide?: boolean; + @property() private _showHelp = false; + + protected render(): TemplateResult | void { + return html` + +
+ + ${this.hass!.localize( + "ui.panel.config.zha.groups_management.header" + )} + + +
+ + ${this.hass!.localize( + "ui.panel.config.zha.groups_management.introduction" + )} + + + +
+ + ${this.hass!.localize("ui.panel.config.zha.common.manage_groups")} + +
+
+
+ `; + } + + private _onHelpTap(): void { + this._showHelp = !this._showHelp; + } + + private _onAddDevicesClick() { + navigate(this, "groups"); + } + + static get styles(): CSSResult[] { + return [ + haStyle, + css` + .content { + margin-top: 24px; + } + + ha-card { + margin: 0 auto; + max-width: 600px; + } + + .card-actions.warning ha-call-service-button { + color: var(--google-red-500); + } + + .toggle-help-icon { + position: absolute; + top: -6px; + right: 0; + color: var(--primary-color); + } + + ha-service-description { + display: block; + color: grey; + } + + [hidden] { + display: none; + } + + .help-text2 { + color: grey; + padding: 16px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-groups-tile": ZHAGroupsTile; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index 3f8110875d9a..939f7c8e4450 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1381,6 +1381,20 @@ "common": { "add_devices": "Add Devices", "clusters": "Clusters", + "zha_zigbee_groups": "ZHA Zigbee Groups", + "manage_groups": "Manage Zigbee Groups", + "groups": "Groups", + "group_id": "Group ID", + "members": "Members", + "add_members": "Add Members", + "remove_members": "Remove Members", + "remove_groups": "Remove Groups", + "create_group": "Create New ZHA Zigbee Group", + "create": "Create Group", + "group_info": "Group Information", + "group_details": "Here are all the details for the selected Zigbee group.", + "create_group_details": "Enter the required details to create a new zigbee group", + "group_name_placeholder": "Group Name", "devices": "Devices", "manufacturer_code_override": "Manufacturer Code Override", "value": "Value" @@ -1395,6 +1409,10 @@ "header": "Network Management", "introduction": "Commands that affect the entire network" }, + "groups_management": { + "header": "Zigbee Group Management", + "introduction": "Create and modify zigbee groups" + }, "node_management": { "header": "Device Management", "introduction": "Run ZHA commands that affect a single device. Pick a device to see a list of available commands.",