Allow editting entity registry (#2630)

* Allow editting entity registry

* Slight simplify

* Style fixes

* Correctly set submitting

* Apply suggestions from code review

Co-Authored-By: balloob <paulus@home-assistant.io>

* Fix invalid type

* Add config section to entity registry

* Trim

* Fix trimming
This commit is contained in:
Paulus Schoutsen 2019-01-30 13:02:41 -08:00 committed by GitHub
parent e42e59871e
commit 175693ba4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 518 additions and 30 deletions

View File

@ -0,0 +1,52 @@
import { HomeAssistant } from "../types";
import computeStateName from "../common/entity/compute_state_name";
export interface EntityRegistryEntry {
entity_id: string;
name: string;
platform: string;
config_entry_id?: string;
device_id?: string;
disabled_by?: string;
}
export interface EntityRegistryEntryUpdateParams {
name: string | null;
new_entity_id: string;
}
export const computeEntityRegistryName = (
hass: HomeAssistant,
entry: EntityRegistryEntry
): string | null => {
if (entry.name) {
return entry.name;
}
const state = hass.states[entry.entity_id];
return state ? computeStateName(state) : null;
};
export const fetchEntityRegistry = (
hass: HomeAssistant
): Promise<EntityRegistryEntry[]> =>
hass.callWS<EntityRegistryEntry[]>({ type: "config/entity_registry/list" });
export const updateEntityRegistryEntry = (
hass: HomeAssistant,
entityId: string,
updates: Partial<EntityRegistryEntryUpdateParams>
): Promise<EntityRegistryEntry> =>
hass.callWS<EntityRegistryEntry>({
type: "config/entity_registry/update",
entity_id: entityId,
...updates,
});
export const removeEntityRegistryEntry = (
hass: HomeAssistant,
entityId: string
): Promise<void> =>
hass.callWS({
type: "config/entity_registry/remove",
entity_id: entityId,
});

View File

@ -11,6 +11,7 @@ import LocalizeMixin from "../../mixins/localize-mixin";
import computeStateName from "../../common/entity/compute_state_name";
import computeDomain from "../../common/entity/compute_domain";
import isComponentLoaded from "../../common/config/is_component_loaded";
import { updateEntityRegistryEntry } from "../../data/entity_registry";
/*
* @appliesMixin EventsMixin
@ -122,12 +123,14 @@ class MoreInfoSettings extends LocalizeMixin(EventsMixin(PolymerElement)) {
async _save() {
try {
const info = await this.hass.callWS({
type: "config/entity_registry/update",
entity_id: this.stateObj.entity_id,
name: this._name,
new_entity_id: this._entityId,
});
const info = await updateEntityRegistryEntry(
this.hass,
this.stateObj.entity_id,
{
name: this._name,
new_entity_id: this._entityId,
}
);
this.registryInfo = info;

View File

@ -1,5 +1,6 @@
import { Constructor, LitElement } from "lit-element";
import { HASSDomEvent, ValidHassDomEvent } from "../../common/dom/fire_event";
import { HassBaseEl } from "./hass-base-mixin";
interface RegisterDialogParams {
dialogShowEvent: keyof HASSDomEvents;
@ -7,6 +8,12 @@ interface RegisterDialogParams {
dialogImport: () => Promise<unknown>;
}
interface ShowDialogParams<T> {
dialogTag: keyof HTMLElementTagNameMap;
dialogImport: () => Promise<unknown>;
dialogParams: T;
}
interface HassDialog<T = HASSDomEvents[ValidHassDomEvent]> extends HTMLElement {
showDialog(params: T);
}
@ -15,20 +22,34 @@ declare global {
// for fire event
interface HASSDomEvents {
"register-dialog": RegisterDialogParams;
"show-dialog": ShowDialogParams<unknown>;
}
// for add event listener
interface HTMLElementEventMap {
"register-dialog": HASSDomEvent<RegisterDialogParams>;
"show-dialog": HASSDomEvent<ShowDialogParams<unknown>>;
}
}
export const dialogManagerMixin = (superClass: Constructor<LitElement>) =>
const LOADED = {};
export const dialogManagerMixin = (
superClass: Constructor<LitElement & HassBaseEl>
) =>
class extends superClass {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
// deprecated
this.addEventListener("register-dialog", (e) =>
this.registerDialog(e.detail)
);
this.addEventListener(
"show-dialog",
async (e: HASSDomEvent<ShowDialogParams<unknown>>) => {
const { dialogTag, dialogImport, dialogParams } = e.detail;
this._showDialog(dialogImport, dialogTag, dialogParams);
}
);
}
private registerDialog({
@ -36,20 +57,29 @@ export const dialogManagerMixin = (superClass: Constructor<LitElement>) =>
dialogTag,
dialogImport,
}: RegisterDialogParams) {
let loaded: Promise<HassDialog<unknown>>;
this.addEventListener(dialogShowEvent, (showEv) => {
if (!loaded) {
loaded = dialogImport().then(() => {
const dialogEl = document.createElement(dialogTag) as HassDialog;
this.shadowRoot!.appendChild(dialogEl);
(this as any).provideHass(dialogEl);
return dialogEl;
});
}
loaded.then((dialogEl) =>
dialogEl.showDialog((showEv as HASSDomEvent<unknown>).detail)
this._showDialog(
dialogImport,
dialogTag,
(showEv as HASSDomEvent<unknown>).detail
);
});
}
private async _showDialog(
dialogImport: () => Promise<unknown>,
dialogTag: string,
dialogParams: unknown
) {
if (!(dialogTag in LOADED)) {
LOADED[dialogTag] = dialogImport().then(() => {
const dialogEl = document.createElement(dialogTag) as HassDialog;
this.shadowRoot!.appendChild(dialogEl);
this.provideHass(dialogEl);
return dialogEl;
});
}
const element = await LOADED[dialogTag];
element.showDialog(dialogParams);
}
};

View File

@ -8,14 +8,8 @@ import "../../../layouts/hass-subpage";
import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixIn from "../../../mixins/localize-mixin";
import computeStateName from "../../../common/entity/compute_state_name";
import "../../../components/entity/state-badge";
function computeEntityName(hass, entity) {
if (entity.name) return entity.name;
const state = hass.states[entity.entity_id];
return state ? computeStateName(state) : null;
}
import { computeEntityRegistryName } from "../../../data/entity_registry";
/*
* @appliesMixin LocalizeMixIn
@ -66,7 +60,7 @@ class HaCeEntitiesCard extends LocalizeMixIn(EventsMixin(PolymerElement)) {
_computeEntityName(entity, hass) {
return (
computeEntityName(hass, entity) ||
computeEntityRegistryName(hass, entity) ||
`(${this.localize(
"ui.panel.config.integrations.config_entry.entity_unavailable"
)})`

View File

@ -10,7 +10,7 @@ import LocalizeMixin from "../../../mixins/localize-mixin";
import isComponentLoaded from "../../../common/config/is_component_loaded";
const CORE_PAGES = ["core", "customize"];
const CORE_PAGES = ["core", "customize", "entity_registry"];
/*
* @appliesMixin LocalizeMixin
* @appliesMixin NavigateMixin
@ -50,7 +50,15 @@ class HaConfigNavigation extends LocalizeMixin(NavigateMixin(PolymerElement)) {
pages: {
type: Array,
value: ["core", "customize", "automation", "script", "zha", "zwave"],
value: [
"core",
"customize",
"entity_registry",
"automation",
"script",
"zha",
"zwave",
],
},
};
}

View File

@ -0,0 +1,191 @@
import {
LitElement,
html,
css,
PropertyDeclarations,
CSSResult,
TemplateResult,
} from "lit-element";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import { EntityRegistryDetailDialogParams } from "./show-dialog-entity-registry-detail";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/ha-style";
import { HomeAssistant } from "../../../types";
import computeDomain from "../../../common/entity/compute_domain";
import { HassEntity } from "home-assistant-js-websocket";
import computeStateName from "../../../common/entity/compute_state_name";
class DialogEntityRegistryDetail extends LitElement {
public hass!: HomeAssistant;
private _name!: string;
private _entityId!: string;
private _error?: string;
private _params?: EntityRegistryDetailDialogParams;
private _submitting?: boolean;
static get properties(): PropertyDeclarations {
return {
_error: {},
_name: {},
_entityId: {},
_params: {},
};
}
public async showDialog(
params: EntityRegistryDetailDialogParams
): Promise<void> {
this._params = params;
this._error = undefined;
this._name = this._params.entry.name || "";
this._entityId = this._params.entry.entity_id;
await this.updateComplete;
}
protected render(): TemplateResult | void {
if (!this._params) {
return html``;
}
const entry = this._params.entry;
const stateObj: HassEntity | undefined = this.hass.states[entry.entity_id];
const invalidDomainUpdate =
computeDomain(this._entityId.trim()) !==
computeDomain(this._params.entry.entity_id);
return html`
<paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
>
<h2>${entry.entity_id}</h2>
<paper-dialog-scrollable>
${!stateObj
? html`
<div>This entity is not currently available.</div>
`
: ""}
${this._error
? html`
<div class="error">${this._error}</div>
`
: ""}
<div class="form">
<paper-input
.value=${this._name}
@value-changed=${this._nameChanged}
.label=${this.hass.localize("ui.dialogs.more_info_settings.name")}
.placeholder=${stateObj ? computeStateName(stateObj) : ""}
.disabled=${this._submitting}
></paper-input>
<paper-input
.value=${this._entityId}
@value-changed=${this._entityIdChanged}
.label=${this.hass.localize(
"ui.dialogs.more_info_settings.entity_id"
)}
error-message="Domain needs to stay the same"
.invalid=${invalidDomainUpdate}
.disabled=${this._submitting}
></paper-input>
</div>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<paper-button
class="danger"
@click="${this._deleteEntry}"
.disabled=${this._submitting}
>
DELETE
</paper-button>
<paper-button
@click="${this._updateEntry}"
.disabled=${invalidDomainUpdate || this._submitting}
>
UPDATE
</paper-button>
</div>
</paper-dialog>
`;
}
private _nameChanged(ev: PolymerChangedEvent<string>): void {
this._error = undefined;
this._name = ev.detail.value;
}
private _entityIdChanged(ev: PolymerChangedEvent<string>): void {
this._error = undefined;
this._entityId = ev.detail.value;
}
private async _updateEntry(): Promise<void> {
try {
this._submitting = true;
await this._params!.updateEntry({
name: this._name.trim() || null,
new_entity_id: this._entityId.trim(),
});
this._params = undefined;
} catch (err) {
this._submitting = false;
this._error = err;
}
}
private async _deleteEntry(): Promise<void> {
this._submitting = true;
if (await this._params!.removeEntry()) {
this._params = undefined;
} else {
this._submitting = false;
}
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!(ev.detail as any).value) {
this._params = undefined;
}
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
paper-dialog {
min-width: 400px;
}
.form {
padding-bottom: 24px;
}
paper-button {
font-weight: 500;
}
paper-button.danger {
font-weight: 500;
color: var(--google-red-500);
margin-left: -12px;
margin-right: auto;
}
.error {
color: var(--google-red-500);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-entity-registry-detail": DialogEntityRegistryDetail;
}
}
customElements.define(
"dialog-entity-registry-detail",
DialogEntityRegistryDetail
);

View File

@ -0,0 +1,165 @@
import {
LitElement,
TemplateResult,
html,
css,
CSSResult,
PropertyDeclarations,
} from "lit-element";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-card/paper-card";
import { HomeAssistant } from "../../../types";
import {
EntityRegistryEntry,
fetchEntityRegistry,
computeEntityRegistryName,
updateEntityRegistryEntry,
removeEntityRegistryEntry,
} from "../../../data/entity_registry";
import "../../../layouts/hass-subpage";
import "../../../layouts/hass-loading-screen";
import "../../../components/ha-icon";
import compare from "../../../common/string/compare";
import domainIcon from "../../../common/entity/domain_icon";
import stateIcon from "../../../common/entity/state_icon";
import computeDomain from "../../../common/entity/compute_domain";
import "../ha-config-section";
import {
showEntityRegistryDetailDialog,
loadEntityRegistryDetailDialog,
} from "./show-dialog-entity-registry-detail";
class HaConfigEntityRegistry extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
private _items?: EntityRegistryEntry[];
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
_items: {},
};
}
protected render(): TemplateResult | void {
if (!this.hass || this._items === undefined) {
return html`
<hass-loading-screen></hass-loading-screen>
`;
}
return html`
<hass-subpage header="Entity Registry">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">Entity Registry</span>
<span slot="introduction">
Home Assistant keeps a registry of every entity it has ever seen
that can be uniquely identified. Each of these entities will have an
entity ID assigned which will be reserved for just this entity.
<p>
Use the entity registry to override the name, change the entity ID
or remove the entry from Home Assistant. Note, removing the entity
registry entry won't remove the entity. To do that, remove it from
<a href="/config/integrations">the integrations page</a>.
</p>
</span>
<paper-card>
${this._items.map((entry) => {
const state = this.hass!.states[entry.entity_id];
return html`
<paper-icon-item @click=${this._openEditEntry} .entry=${entry}>
<ha-icon
slot="item-icon"
.icon=${state
? stateIcon(state)
: domainIcon(computeDomain(entry.entity_id))}
></ha-icon>
<paper-item-body two-line>
<div class="name">
${computeEntityRegistryName(this.hass!, entry) ||
"(unavailable)"}
</div>
<div class="secondary entity-id">
${entry.entity_id}
</div>
</paper-item-body>
<div class="platform">${entry.platform}</div>
</paper-icon-item>
`;
})}
</paper-card>
</ha-config-section>
</hass-subpage>
`;
}
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
this._fetchData();
loadEntityRegistryDetailDialog();
}
private async _fetchData(): Promise<void> {
this._items = (await fetchEntityRegistry(this.hass!)).sort((ent1, ent2) =>
compare(ent1.entity_id, ent2.entity_id)
);
}
private _openEditEntry(ev: MouseEvent): void {
const entry = (ev.currentTarget! as any).entry;
showEntityRegistryDetailDialog(this, {
entry,
updateEntry: async (updates) => {
const updated = await updateEntityRegistryEntry(
this.hass!,
entry.entity_id,
updates
);
this._items = this._items!.map((ent) =>
ent === entry ? updated : ent
);
},
removeEntry: async () => {
if (
!confirm(`Are you sure you want to delete this entry?
Deleting an entry will not remove the entity from Home Assistant. To do this, you will need to remove the integration "${
entry.platform
}" from Home Assistant.`)
) {
return false;
}
try {
await removeEntityRegistryEntry(this.hass!, entry.entity_id);
this._items = this._items!.filter((ent) => ent !== entry);
return true;
} catch (err) {
return false;
}
},
});
}
static get styles(): CSSResult {
return css`
a {
color: var(--primary-color);
}
paper-card {
display: block;
background-color: white;
}
paper-icon-item {
cursor: pointer;
}
ha-icon {
margin-left: 8px;
}
`;
}
}
customElements.define("ha-config-entity-registry", HaConfigEntityRegistry);

View File

@ -0,0 +1,27 @@
import { fireEvent } from "../../../common/dom/fire_event";
import {
EntityRegistryEntry,
EntityRegistryEntryUpdateParams,
} from "../../../data/entity_registry";
export interface EntityRegistryDetailDialogParams {
entry: EntityRegistryEntry;
updateEntry: (
updates: Partial<EntityRegistryEntryUpdateParams>
) => Promise<unknown>;
removeEntry: () => Promise<boolean>;
}
export const loadEntityRegistryDetailDialog = () =>
import(/* webpackChunkName: "entity-registry-detail-dialog" */ "./dialog-entity-registry-detail");
export const showEntityRegistryDetailDialog = (
element: HTMLElement,
systemLogDetailParams: EntityRegistryDetailDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-entity-registry-detail",
dialogImport: loadEntityRegistryDetailDialog,
dialogParams: systemLogDetailParams,
});
};

View File

@ -16,6 +16,7 @@ import(/* webpackChunkName: "panel-config-core" */ "./core/ha-config-core");
import(/* webpackChunkName: "panel-config-customize" */ "./customize/ha-config-customize");
import(/* webpackChunkName: "panel-config-dashboard" */ "./dashboard/ha-config-dashboard");
import(/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script");
import(/* webpackChunkName: "panel-config-entity-registry" */ "./entity_registry/ha-config-entity-registry");
import(/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users");
import(/* webpackChunkName: "panel-config-zha" */ "./zha/ha-config-zha");
import(/* webpackChunkName: "panel-config-zwave" */ "./zwave/ha-config-zwave");
@ -92,6 +93,19 @@ class HaPanelConfig extends EventsMixin(NavigateMixin(PolymerElement)) {
></ha-config-script>
</template>
<template
is="dom-if"
if='[[_equals(_routeData.page, "entity_registry")]]'
restamp
>
<ha-config-entity-registry
page-name="entity_registry"
route="[[route]]"
hass="[[hass]]"
is-wide="[[isWide]]"
></ha-config-entity-registry>
</template>
<template is="dom-if" if='[[_equals(_routeData.page, "zha")]]' restamp>
<ha-config-zha
page-name="zha"

View File

@ -491,7 +491,7 @@
},
"more_info_settings": {
"save": "Save",
"name": "Name",
"name": "Name Override",
"entity_id": "Entity ID"
}
},
@ -748,6 +748,10 @@
"description_login": "Logged in as {email}",
"description_not_login": "Not logged in"
},
"entity_registry": {
"caption": "Entity Registry",
"description": "Overview of all known entities."
},
"integrations": {
"caption": "Integrations",
"description": "Manage connected devices and services",