From 3bdab738c62773616199479c137e8a37739e47f2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Dec 2020 15:40:35 +0100 Subject: [PATCH] Support disabling devices (#7715) Co-authored-by: Bram Kragten --- src/components/device/ha-device-picker.ts | 4 +- src/data/device_registry.ts | 2 + .../dialog-device-registry-detail.ts | 47 ++- .../config/devices/ha-config-device-page.ts | 61 +++- .../devices/ha-config-devices-dashboard.ts | 284 +++++++++++++++++- src/translations/en.json | 21 +- 6 files changed, 399 insertions(+), 20 deletions(-) diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index fa7f1e70f1..34897f2c5a 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -156,7 +156,9 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { areaLookup[area.area_id] = area; } - let inputDevices = [...devices]; + let inputDevices = devices.filter( + (device) => device.id === this.value || !device.disabled_by + ); if (includeDomains) { inputDevices = inputDevices.filter((device) => { diff --git a/src/data/device_registry.ts b/src/data/device_registry.ts index 215110df1f..7d52f8dea9 100644 --- a/src/data/device_registry.ts +++ b/src/data/device_registry.ts @@ -17,6 +17,7 @@ export interface DeviceRegistryEntry { area_id?: string; name_by_user?: string; entry_type: "service" | null; + disabled_by: string | null; } export interface DeviceEntityLookup { @@ -26,6 +27,7 @@ export interface DeviceEntityLookup { export interface DeviceRegistryEntryMutableParams { area_id?: string | null; name_by_user?: string | null; + disabled_by?: string | null; } export const fallbackDeviceName = ( diff --git a/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts b/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts index ba05ad3c66..4fd6abcb7c 100644 --- a/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts +++ b/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts @@ -19,10 +19,11 @@ import { import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail"; import { HomeAssistant } from "../../../../types"; +import type { HaSwitch } from "../../../../components/ha-switch"; import { PolymerChangedEvent } from "../../../../polymer-types"; import { computeDeviceName } from "../../../../data/device_registry"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { haStyleDialog } from "../../../../resources/styles"; +import { haStyle, haStyleDialog } from "../../../../resources/styles"; @customElement("dialog-device-registry-detail") class DialogDeviceRegistryDetail extends LitElement { @@ -36,6 +37,8 @@ class DialogDeviceRegistryDetail extends LitElement { @internalProperty() private _areaId?: string; + @internalProperty() private _disabledBy!: string | null; + @internalProperty() private _submitting?: boolean; public async showDialog( @@ -45,6 +48,7 @@ class DialogDeviceRegistryDetail extends LitElement { this._error = undefined; this._nameByUser = this._params.device.name_by_user || ""; this._areaId = this._params.device.area_id; + this._disabledBy = this._params.device.disabled_by; await this.updateComplete; } @@ -80,6 +84,32 @@ class DialogDeviceRegistryDetail extends LitElement { .value=${this._areaId} @value-changed=${this._areaPicked} > +
+ + +
+
+ ${this.hass.localize("ui.panel.config.devices.enabled_label")} +
+
+ ${this._disabledBy && this._disabledBy !== "user" + ? this.hass.localize( + "ui.panel.config.devices.enabled_cause", + "cause", + this.hass.localize( + `config_entry.disabled_by.${this._disabledBy}` + ) + ) + : ""} + ${this.hass.localize( + "ui.panel.config.devices.enabled_description" + )} +
+
+
{ this._submitting = true; try { await this._params!.updateEntry({ name_by_user: this._nameByUser.trim() || null, area_id: this._areaId || null, + disabled_by: this._disabledBy || null, }); this._params = undefined; } catch (err) { @@ -128,6 +163,7 @@ class DialogDeviceRegistryDetail extends LitElement { static get styles(): CSSResult[] { return [ + haStyle, haStyleDialog, css` .form { @@ -139,6 +175,15 @@ class DialogDeviceRegistryDetail extends LitElement { .error { color: var(--error-color); } + ha-switch { + margin-right: 16px; + } + .row { + margin-top: 8px; + color: var(--primary-text-color); + display: flex; + align-items: center; + } `, ]; } diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 84ed1860c5..560e1bc4e9 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -246,6 +246,26 @@ export class HaConfigDevicePage extends LitElement { .devices=${this.devices} .device=${device} > + ${ + device.disabled_by + ? html` +
+

+ ${this.hass.localize( + "ui.panel.config.devices.enabled_cause", + "cause", + device.disabled_by + )} +

+
+
+ + ${this.hass.localize("ui.common.enable")} + +
+ ` + : html`` + } ${this._renderIntegrationInfo(device, integrations)} @@ -272,9 +292,14 @@ export class HaConfigDevicePage extends LitElement { )} @@ -342,9 +367,16 @@ export class HaConfigDevicePage extends LitElement { @@ -415,9 +447,14 @@ export class HaConfigDevicePage extends LitElement { )} @@ -632,6 +669,12 @@ export class HaConfigDevicePage extends LitElement { }); } + private async _enableDevice(): Promise { + await updateDeviceRegistryEntry(this.hass, this.deviceId, { + disabled_by: null, + }); + } + static get styles(): CSSResult { return css` .container { diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index c75a8c6f95..a8d7794aaa 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -1,5 +1,8 @@ -import { mdiPlus } from "@mdi/js"; +import { mdiPlus, mdiFilterVariant } from "@mdi/js"; +import "@material/mwc-list/mwc-list-item"; import { + css, + CSSResult, customElement, html, internalProperty, @@ -7,7 +10,9 @@ import { property, TemplateResult, } from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; import memoizeOne from "memoize-one"; +import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; import { HASSDomEvent } from "../../../common/dom/fire_event"; import { navigate } from "../../../common/navigate"; import { LocalizeFunc } from "../../../common/translations/localize"; @@ -18,6 +23,7 @@ import { RowClickedEvent, } from "../../../components/data-table/ha-data-table"; import "../../../components/entity/ha-battery-icon"; +import "../../../components/ha-button-menu"; import { AreaRegistryEntry } from "../../../data/area_registry"; import { ConfigEntry } from "../../../data/config_entries"; import { @@ -34,6 +40,7 @@ import { domainToName } from "../../../data/integration"; import "../../../layouts/hass-tabs-subpage-data-table"; import { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; +import { haStyle } from "../../../resources/styles"; interface DeviceRowData extends DeviceRegistryEntry { device?: DeviceRowData; @@ -64,6 +71,12 @@ export class HaConfigDeviceDashboard extends LitElement { window.location.search ); + @internalProperty() private _showDisabled = false; + + @internalProperty() private _filter = ""; + + @internalProperty() private _numHiddenDevices = 0; + private _activeFilters = memoizeOne( ( entries: ConfigEntry[], @@ -74,6 +87,10 @@ export class HaConfigDeviceDashboard extends LitElement { filters.forEach((value, key) => { switch (key) { case "config_entry": { + // If we are requested to show the devices for a given config entry, + // also show the disabled ones by default. + this._showDisabled = true; + const configEntry = entries.find( (entry) => entry.entry_id === value ); @@ -105,6 +122,7 @@ export class HaConfigDeviceDashboard extends LitElement { entities: EntityRegistryEntry[], areas: AreaRegistryEntry[], filters: URLSearchParams, + showDisabled: boolean, localize: LocalizeFunc ) => { // Some older installations might have devices pointing at invalid entryIDs @@ -117,6 +135,9 @@ export class HaConfigDeviceDashboard extends LitElement { deviceLookup[device.id] = device; } + // If nothing gets filtered, this is our correct count of devices + let startLength = outputDevices.length; + const deviceEntityLookup: DeviceEntityLookup = {}; for (const entity of entities) { if (!entity.device_id) { @@ -145,6 +166,7 @@ export class HaConfigDeviceDashboard extends LitElement { outputDevices = outputDevices.filter((device) => device.config_entries.includes(value) ); + startLength = outputDevices.length; const configEntry = entries.find((entry) => entry.entry_id === value); if (configEntry) { filterDomains.push(configEntry.domain); @@ -152,6 +174,10 @@ export class HaConfigDeviceDashboard extends LitElement { } }); + if (!showDisabled) { + outputDevices = outputDevices.filter((device) => !device.disabled_by); + } + outputDevices = outputDevices.map((device) => { return { ...device, @@ -182,6 +208,7 @@ export class HaConfigDeviceDashboard extends LitElement { }; }); + this._numHiddenDevices = startLength - outputDevices.length; return { devicesOutput: outputDevices, filteredDomains: filterDomains }; } ); @@ -298,9 +325,119 @@ export class HaConfigDeviceDashboard extends LitElement { this.entities, this.areas, this._searchParms, + this._showDisabled, this.hass.localize ); const includeZHAFab = filteredDomains.includes("zha"); + const activeFilters = this._activeFilters( + this.entries, + this._searchParms, + this.hass.localize + ); + + const headerToolbar = html` + ${activeFilters + ? html`
+ ${this.narrow + ? html`
+ + + ${this.hass.localize( + "ui.panel.config.filtering.filtering_by" + )} + ${activeFilters.join(", ")} + ${this._numHiddenDevices + ? "(" + + this.hass.localize( + "ui.panel.config.devices.picker.filter.hidden_devices", + "number", + this._numHiddenDevices + ) + + ")" + : ""} + +
` + : `${this.hass.localize( + "ui.panel.config.filtering.filtering_by" + )} ${activeFilters.join(", ")} + ${ + this._numHiddenDevices + ? "(" + + this.hass.localize( + "ui.panel.config.devices.picker.filter.hidden_devices", + "number", + this._numHiddenDevices + ) + + ")" + : "" + } + `} + ${this.hass.localize( + "ui.panel.config.filtering.clear" + )} +
` + : ""} + ${this._numHiddenDevices && !activeFilters + ? html`
+ ${this.narrow + ? html`
+ + + ${this.hass.localize( + "ui.panel.config.devices.picker.filter.hidden_devices", + "number", + this._numHiddenDevices + )} + +
` + : `${this.hass.localize( + "ui.panel.config.devices.picker.filter.hidden_devices", + "number", + this._numHiddenDevices + )}`} + ${this.hass.localize( + "ui.panel.config.devices.picker.filter.show_all" + )} +
` + : ""} + + + + + + + ${this.hass!.localize( + "ui.panel.config.devices.picker.filter.show_disabled" + )} + + + `; return html` ` : html``} +
+ ${headerToolbar} +
`; } @@ -363,6 +505,136 @@ export class HaConfigDeviceDashboard extends LitElement { const deviceId = ev.detail.id; navigate(this, `/config/devices/device/${deviceId}`); } + + private _showDisabledChanged(ev: CustomEvent) { + if (ev.detail.source !== "property") { + return; + } + this._showDisabled = ev.detail.selected; + } + + private _handleSearchChange(ev: CustomEvent) { + this._filter = ev.detail.value; + } + + private _clearFilter() { + navigate(this, window.location.pathname, true); + } + + private _showAll() { + this._showDisabled = true; + } + + static get styles(): CSSResult[] { + return [ + haStyle, + css` + hass-loading-screen { + --app-header-background-color: var(--sidebar-background-color); + --app-header-text-color: var(--sidebar-text-color); + } + a { + color: var(--primary-color); + } + h2 { + margin-top: 0; + font-family: var(--paper-font-headline_-_font-family); + -webkit-font-smoothing: var( + --paper-font-headline_-_-webkit-font-smoothing + ); + font-size: var(--paper-font-headline_-_font-size); + font-weight: var(--paper-font-headline_-_font-weight); + letter-spacing: var(--paper-font-headline_-_letter-spacing); + line-height: var(--paper-font-headline_-_line-height); + opacity: var(--dark-primary-opacity); + } + p { + font-family: var(--paper-font-subhead_-_font-family); + -webkit-font-smoothing: var( + --paper-font-subhead_-_-webkit-font-smoothing + ); + font-weight: var(--paper-font-subhead_-_font-weight); + line-height: var(--paper-font-subhead_-_line-height); + } + ha-data-table { + width: 100%; + --data-table-border-width: 0; + } + :host(:not([narrow])) ha-data-table { + height: calc(100vh - 1px - var(--header-height)); + display: block; + } + ha-button-menu { + margin-right: 8px; + } + .table-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12); + } + search-input { + margin-left: 16px; + flex-grow: 1; + position: relative; + top: 2px; + } + .search-toolbar search-input { + margin-left: 8px; + top: 1px; + } + .search-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + color: var(--secondary-text-color); + } + .search-toolbar ha-button-menu { + position: static; + } + .selected-txt { + font-weight: bold; + padding-left: 16px; + } + .table-header .selected-txt { + margin-top: 20px; + } + .search-toolbar .selected-txt { + font-size: 16px; + } + .header-btns > mwc-button, + .header-btns > ha-icon-button { + margin: 8px; + } + .active-filters { + color: var(--primary-text-color); + position: relative; + display: flex; + align-items: center; + padding: 2px 2px 2px 8px; + margin-left: 4px; + font-size: 14px; + } + .active-filters ha-icon { + color: var(--primary-color); + } + .active-filters mwc-button { + margin-left: 8px; + } + .active-filters::before { + background-color: var(--primary-color); + opacity: 0.12; + border-radius: 4px; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + content: ""; + } + `, + ]; + } } declare global { diff --git a/src/translations/en.json b/src/translations/en.json index 77ac8443e3..46a4aaf94b 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1755,11 +1755,15 @@ "name": "Name", "update": "Update", "no_devices": "No devices", + "enabled_label": "Enable device", + "enabled_cause": "Disabled by {cause}.", + "enabled_description": "Disabled devices will not be shown and entities belonging to the device will be disabled and not added to Home Assistant.", "automation": { "automations": "Automations", "no_automations": "No automations", "unknown_automation": "Unknown automation", "create": "Create automation with device", + "create_disable": "Can't create automation with disabled device", "triggers": { "caption": "Do something when...", "no_triggers": "No triggers", @@ -1780,12 +1784,14 @@ "script": { "scripts": "Scripts", "no_scripts": "No scripts", - "create": "Create script with device" + "create": "Create script with device", + "create_disable": "Can't create script with disabled device" }, "scene": { "scenes": "Scenes", "no_scenes": "No scenes", - "create": "Create scene with device" + "create": "Create scene with device", + "create_disable": "Can't create scene with disabled device" }, "cant_edit": "You can only edit items that are created in the UI.", "device_not_found": "Device not found.", @@ -1811,7 +1817,16 @@ "no_area": "No area" }, "delete": "Delete", - "confirm_delete": "Are you sure you want to delete this device?" + "confirm_delete": "Are you sure you want to delete this device?", + "picker": { + "search": "Search devices", + "filter": { + "filter": "Filter", + "show_disabled": "Show disabled devices", + "hidden_devices": "{number} hidden {number, plural,\n one {device}\n other {devices}\n}", + "show_all": "Show all" + } + } }, "entities": { "caption": "Entities",