Add UI for setting an area on entity level (#7837)

This commit is contained in:
Bram Kragten 2020-11-29 22:00:51 +01:00 committed by GitHub
parent 7ceb6eb50d
commit fe31d15d27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 246 additions and 43 deletions

View File

@ -137,8 +137,7 @@ export class DialogHassioNetwork extends LitElement
)}
${this._interface?.type === "wireless"
? html`
<ha-expansion-panel outlined>
<span slot="title">Wi-Fi</span>
<ha-expansion-panel header="Wi-Fi" outlined>
${this._interface?.wifi?.ssid
? html`<p>Connected to: ${this._interface?.wifi?.ssid}</p>`
: ""}
@ -281,8 +280,10 @@ export class DialogHassioNetwork extends LitElement
private _renderIPConfiguration(version: string) {
return html`
<ha-expansion-panel outlined>
<span slot="title">IPv${version.charAt(version.length - 1)}</span>
<ha-expansion-panel
.header=${`IPv${version.charAt(version.length - 1)}`}
outlined
>
<div class="radio-row">
<ha-formfield label="DHCP">
<ha-radio
@ -591,6 +592,7 @@ export class DialogHassioNetwork extends LitElement
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 16px;
margin: 4px 0;
}
paper-input {

View File

@ -28,6 +28,7 @@ import {
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import memoizeOne from "memoize-one";
const rowRenderer = (
root: HTMLElement,
@ -68,6 +69,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@property() public value?: string;
@property() public placeholder?: string;
@property() public _areas?: AreaRegistryEntry[];
@property({ type: Boolean, attribute: "no-add" })
@ -110,6 +113,9 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.area-picker.area")
: this.label}
.placeholder=${this.placeholder
? this._area(this.placeholder)?.name
: undefined}
class="input"
autocapitalize="none"
autocomplete="off"
@ -151,6 +157,12 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
`;
}
private _area = memoizeOne((areaId: string):
| AreaRegistryEntry
| undefined => {
return this._areas?.find((area) => area.area_id === areaId);
});
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue("");

View File

@ -19,12 +19,14 @@ class HaExpansionPanel extends LitElement {
@property({ type: Boolean, reflect: true }) outlined = false;
@property() header?: string;
@query(".container") private _container!: HTMLDivElement;
protected render(): TemplateResult {
return html`
<div class="summary" @click=${this._toggleContainer}>
<slot name="title"></slot>
<slot name="header">${this.header}</slot>
<ha-svg-icon
.path=${mdiChevronDown}
class="summary-icon ${classMap({ expanded: this.expanded })}"
@ -76,7 +78,7 @@ class HaExpansionPanel extends LitElement {
.summary {
display: flex;
padding: var(--expansion-panel-summary-padding, 0px 16px);
padding: var(--expansion-panel-summary-padding, 0);
min-height: 48px;
align-items: center;
cursor: pointer;

View File

@ -10,6 +10,7 @@ export interface EntityRegistryEntry {
platform: string;
config_entry_id?: string;
device_id?: string;
area_id?: string;
disabled_by: string | null;
}
@ -29,6 +30,7 @@ export interface UpdateEntityRegistryEntryResult {
export interface EntityRegistryEntryUpdateParams {
name?: string | null;
icon?: string | null;
area_id?: string | null;
disabled_by?: string | null;
new_entity_id?: string;
}

View File

@ -4,7 +4,6 @@ import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import "../../../components/ha-circular-progress";
import {
css,
CSSResult,
customElement,
html,
@ -97,12 +96,11 @@ class DialogImportBlueprint extends LitElement {
)}
></paper-input>
`}
<ha-expansion-panel>
<span slot="title"
>${this.hass.localize(
"ui.panel.config.blueprint.add.raw_blueprint"
)}</span
>
<ha-expansion-panel
.header=${this.hass.localize(
"ui.panel.config.blueprint.add.raw_blueprint"
)}
>
<pre>${this._result.raw_data}</pre>
</ha-expansion-panel>`
: html`${this.hass.localize(
@ -201,15 +199,8 @@ class DialogImportBlueprint extends LitElement {
}
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
ha-expansion-panel {
--expansion-panel-summary-padding: 0;
}
`,
];
static get styles(): CSSResult {
return haStyleDialog;
}
}

View File

@ -12,7 +12,7 @@ import {
computeDeviceName,
DeviceRegistryEntry,
} from "../../../../data/device_registry";
import { loadDeviceRegistryDetailDialog } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
import { loadDeviceRegistryDetailDialog } from "../device-registry-detail/show-dialog-device-registry-detail";
import { HomeAssistant } from "../../../../types";
@customElement("ha-device-info-card")

View File

@ -3,8 +3,8 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "../../components/ha-dialog";
import "../../components/ha-area-picker";
import "../../../../components/ha-dialog";
import "../../../../components/ha-area-picker";
import {
CSSResult,
@ -18,11 +18,11 @@ import {
} from "lit-element";
import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail";
import { HomeAssistant } from "../../types";
import { PolymerChangedEvent } from "../../polymer-types";
import { computeDeviceName } from "../../data/device_registry";
import { fireEvent } from "../../common/dom/fire_event";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { PolymerChangedEvent } from "../../../../polymer-types";
import { computeDeviceName } from "../../../../data/device_registry";
import { fireEvent } from "../../../../common/dom/fire_event";
import { haStyleDialog } from "../../../../resources/styles";
@customElement("dialog-device-registry-detail")
class DialogDeviceRegistryDetail extends LitElement {

View File

@ -1,8 +1,8 @@
import { fireEvent } from "../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import {
DeviceRegistryEntry,
DeviceRegistryEntryMutableParams,
} from "../../data/device_registry";
} from "../../../../data/device_registry";
export interface DeviceRegistryDetailDialogParams {
device: DeviceRegistryEntry;

View File

@ -35,7 +35,7 @@ import { findRelated, RelatedResult } from "../../../data/search";
import {
loadDeviceRegistryDetailDialog,
showDeviceRegistryDetailDialog,
} from "../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
} from "./device-registry-detail/show-dialog-device-registry-detail";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-tabs-subpage";

View File

@ -20,9 +20,16 @@ import {
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import type { PolymerChangedEvent } from "../../../polymer-types";
import type { HomeAssistant } from "../../../types";
import "../../../components/ha-area-picker";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
@customElement("ha-registry-basic-editor")
export class HaEntityRegistryBasicEditor extends LitElement {
export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entry!: ExtEntityRegistryEntry;
@ -31,16 +38,26 @@ export class HaEntityRegistryBasicEditor extends LitElement {
@internalProperty() private _entityId!: string;
@internalProperty() private _areaId?: string;
@internalProperty() private _disabledBy!: string | null;
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
@internalProperty() private _device?: DeviceRegistryEntry;
@internalProperty() private _submitting?: boolean;
public async updateEntry(): Promise<void> {
this._submitting = true;
const params: Partial<EntityRegistryEntryUpdateParams> = {
new_entity_id: this._entityId.trim(),
area_id: this._areaId || null,
};
if (this._disabledBy === null || this._disabledBy === "user") {
if (
this.entry.disabled_by !== this._disabledBy &&
(this._disabledBy === null || this._disabledBy === "user")
) {
params.disabled_by = this._disabledBy;
}
try {
@ -70,6 +87,20 @@ export class HaEntityRegistryBasicEditor extends LitElement {
}
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._deviceLookup = {};
for (const device of devices) {
this._deviceLookup[device.id] = device;
}
if (!this._device && this.entry.device_id) {
this._device = this._deviceLookup[this.entry.device_id];
}
}),
];
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (!changedProperties.has("entry")) {
@ -79,6 +110,11 @@ export class HaEntityRegistryBasicEditor extends LitElement {
this._origEntityId = this.entry.entity_id;
this._entityId = this.entry.entity_id;
this._disabledBy = this.entry.disabled_by;
this._areaId = this.entry.area_id;
this._device =
this.entry.device_id && this._deviceLookup
? this._deviceLookup[this.entry.device_id]
: undefined;
}
}
@ -105,6 +141,12 @@ export class HaEntityRegistryBasicEditor extends LitElement {
.invalid=${invalidDomainUpdate}
.disabled=${this._submitting}
></paper-input>
<ha-area-picker
.hass=${this.hass}
.value=${this._areaId}
.placeholder=${this._device?.area_id}
@value-changed=${this._areaPicked}
></ha-area-picker>
<div class="row">
<ha-switch
.checked=${!this._disabledBy}
@ -139,6 +181,10 @@ export class HaEntityRegistryBasicEditor extends LitElement {
`;
}
private _areaPicked(ev: CustomEvent) {
this._areaId = ev.detail.value;
}
private _entityIdChanged(ev: PolymerChangedEvent<string>): void {
this._entityId = ev.detail.value;
}

View File

@ -1,6 +1,6 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input";
import { HassEntity } from "home-assistant-js-websocket";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResult,
@ -31,9 +31,18 @@ import type { PolymerChangedEvent } from "../../../polymer-types";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { domainIcon } from "../../../common/entity/domain_icon";
import "../../../components/ha-area-picker";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
updateDeviceRegistryEntry,
} from "../../../data/device_registry";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import "../../../components/ha-expansion-panel";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
@customElement("entity-registry-settings")
export class EntityRegistrySettings extends LitElement {
export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entry!: ExtEntityRegistryEntry;
@ -44,14 +53,34 @@ export class EntityRegistrySettings extends LitElement {
@internalProperty() private _entityId!: string;
@internalProperty() private _areaId?: string | null;
@internalProperty() private _disabledBy!: string | null;
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
@internalProperty() private _device?: DeviceRegistryEntry;
@internalProperty() private _error?: string;
@internalProperty() private _submitting?: boolean;
private _origEntityId!: string;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._deviceLookup = {};
for (const device of devices) {
this._deviceLookup[device.id] = device;
}
if (this.entry.device_id) {
this._device = this._deviceLookup[this.entry.device_id];
}
}),
];
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("entry")) {
@ -59,8 +88,13 @@ export class EntityRegistrySettings extends LitElement {
this._name = this.entry.name || "";
this._icon = this.entry.icon || "";
this._origEntityId = this.entry.entity_id;
this._areaId = this.entry.area_id;
this._entityId = this.entry.entity_id;
this._disabledBy = this.entry.disabled_by;
this._device =
this.entry.device_id && this._deviceLookup
? this._deviceLookup[this.entry.device_id]
: undefined;
}
}
@ -117,6 +151,13 @@ export class EntityRegistrySettings extends LitElement {
.invalid=${invalidDomainUpdate}
.disabled=${this._submitting}
></paper-input>
${!this.entry.device_id
? html`<ha-area-picker
.hass=${this.hass}
.value=${this._areaId}
@value-changed=${this._areaPicked}
></ha-area-picker>`
: ""}
<div class="row">
<ha-switch
.checked=${!this._disabledBy}
@ -148,6 +189,31 @@ export class EntityRegistrySettings extends LitElement {
</div>
</div>
</div>
${this.entry.device_id
? html`<ha-expansion-panel .header=${"Advanced"}>
<p>
By default the entities of a device are in the same area as the
device. If you change the area of this entity, it will no longer
follow the area of the device.
</p>
${this._areaId
? html`<mwc-button @click=${this._clearArea}
>Follow device area</mwc-button
>`
: this._device
? html`<mwc-button @click=${this._openDeviceSettings}
>Change device area</mwc-button
>`
: ""}
<ha-area-picker
.hass=${this.hass}
.value=${this._areaId}
.placeholder=${this._device?.area_id}
@value-changed=${this._areaPicked}
></ha-area-picker
></ha-expansion-panel>`
: ""}
</div>
<div class="buttons">
<mwc-button
@ -183,14 +249,37 @@ export class EntityRegistrySettings extends LitElement {
this._entityId = ev.detail.value;
}
private _areaPicked(ev: CustomEvent) {
this._error = undefined;
this._areaId = ev.detail.value;
}
private _clearArea() {
this._error = undefined;
this._areaId = null;
}
private _openDeviceSettings() {
showDeviceRegistryDetailDialog(this, {
device: this._device!,
updateEntry: async (updates) => {
await updateDeviceRegistryEntry(this.hass, this._device!.id, updates);
},
});
}
private async _updateEntry(): Promise<void> {
this._submitting = true;
const params: Partial<EntityRegistryEntryUpdateParams> = {
name: this._name.trim() || null,
icon: this._icon.trim() || null,
area_id: this._areaId || null,
new_entity_id: this._entityId.trim(),
};
if (this._disabledBy === null || this._disabledBy === "user") {
if (
this.entry.disabled_by !== this._disabledBy &&
(this._disabledBy === null || this._disabledBy === "user")
) {
params.disabled_by = this._disabledBy;
}
try {

View File

@ -62,6 +62,14 @@ import {
} from "./show-dialog-entity-editor";
import { haStyle } from "../../../resources/styles";
import { UNAVAILABLE } from "../../../data/entity";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../../data/area_registry";
export interface StateEntity extends EntityRegistryEntry {
readonly?: boolean;
@ -73,6 +81,7 @@ export interface EntityRow extends StateEntity {
unavailable: boolean;
restored: boolean;
status: string;
area?: string;
}
@customElement("ha-config-entities")
@ -87,6 +96,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
@internalProperty() private _entities?: EntityRegistryEntry[];
@internalProperty() private _devices?: DeviceRegistryEntry[];
@internalProperty() private _areas: AreaRegistryEntry[] = [];
@internalProperty() private _stateEntities: StateEntity[] = [];
@property() public _entries?: ConfigEntry[];
@ -201,6 +214,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
template: (platform) =>
this.hass.localize(`component.${platform}.title`) || platform,
},
area: {
title: this.hass.localize(
"ui.panel.config.entities.picker.headers.area"
),
sortable: true,
hidden: narrow,
filterable: true,
width: "15%",
},
status: {
title: this.hass.localize(
"ui.panel.config.entities.picker.headers.status"
@ -255,6 +277,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
private _filteredEntities = memoize(
(
entities: EntityRegistryEntry[],
devices: DeviceRegistryEntry[] | undefined,
areas: AreaRegistryEntry[] | undefined,
stateEntities: StateEntity[],
filters: URLSearchParams,
showDisabled: boolean,
@ -262,21 +286,42 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
showReadOnly: boolean
): EntityRow[] => {
const result: EntityRow[] = [];
// If nothing gets filtered, this is our correct count of entities
let startLength = entities.length + stateEntities.length;
entities = showReadOnly ? entities.concat(stateEntities) : entities;
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
if (areas) {
for (const area of areas) {
areaLookup[area.area_id] = area;
}
if (devices) {
for (const device of devices) {
deviceLookup[device.id] = device;
}
}
}
entities.forEach((entity) => {
return entity;
});
let filteredEntities = showReadOnly
? entities.concat(stateEntities)
: entities;
filters.forEach((value, key) => {
switch (key) {
case "config_entry":
entities = entities.filter(
filteredEntities = filteredEntities.filter(
(entity) => entity.config_entry_id === value
);
// If we have an active filter and `showReadOnly` is true, the length of `entities` is correct.
// If however, the read-only entities were not added before, we need to check how many would
// have matched the active filter and add that number to the count.
startLength = entities.length;
startLength = filteredEntities.length;
if (!showReadOnly) {
startLength += stateEntities.filter(
(entity) => entity.config_entry_id === value
@ -287,13 +332,17 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
});
if (!showDisabled) {
entities = entities.filter((entity) => !entity.disabled_by);
filteredEntities = filteredEntities.filter(
(entity) => !entity.disabled_by
);
}
for (const entry of entities) {
for (const entry of filteredEntities) {
const entity = this.hass.states[entry.entity_id];
const unavailable = entity?.state === UNAVAILABLE;
const restored = entity?.attributes.restored;
const areaId = entry.area_id ?? deviceLookup[entry.device_id!]?.area_id;
const area = areaId ? areaLookup[areaId] : undefined;
if (!showUnavailable && unavailable) {
continue;
@ -309,6 +358,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this.hass.localize("state.default.unavailable"),
unavailable,
restored,
area: area ? area.name : undefined,
status: restored
? this.hass.localize(
"ui.panel.config.entities.picker.status.restored"
@ -345,6 +395,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities;
}),
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._devices = devices;
}),
subscribeAreaRegistry(this.hass.connection, (areas) => {
this._areas = areas;
}),
];
}
@ -372,6 +428,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
const entityData = this._filteredEntities(
this._entities,
this._devices,
this._areas,
this._stateEntities,
this._searchParms,
this._showDisabled,

View File

@ -1830,6 +1830,7 @@
"name": "Name",
"entity_id": "Entity ID",
"integration": "Integration",
"area": "Area",
"status": "Status"
},
"selected": "{number} selected",