Hide empty blocks on device page (#3950)

* Hide empty blocks on device page

* lint

* Rename entities on device rename

* check if entity_id is valid

* clarify var name

* Review comments

* Use regex to replace not allowed chars

* Align with backend
This commit is contained in:
Bram Kragten 2019-10-08 17:53:31 +02:00 committed by Paulus Schoutsen
parent 12d8a04c15
commit 9ad7f0dbac
13 changed files with 198 additions and 91 deletions

View File

@ -1,2 +1,12 @@
const validEntityId = /^(\w+)\.(\w+)$/; const validEntityId = /^(\w+)\.(\w+)$/;
export default (entityId: string) => validEntityId.test(entityId);
export const isValidEntityId = (entityId: string) =>
validEntityId.test(entityId);
export const createValidEntityId = (input: string) =>
input
.toLowerCase()
.replace(/\s|\'/g, "_") // replace spaces and quotes with underscore
.replace(/\W/g, "") // remove not allowed chars
.replace(/_{2,}/g, "_") // replace multiple underscores with 1
.replace(/_$/, ""); // remove underscores at the end

View File

@ -10,7 +10,7 @@ import "@polymer/paper-icon-button/paper-icon-button-light";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import isValidEntityId from "../../common/entity/valid_entity_id"; import { isValidEntityId } from "../../common/entity/valid_entity_id";
import "./ha-entity-picker"; import "./ha-entity-picker";
// Not a duplicate, type import // Not a duplicate, type import

View File

@ -1,7 +1,6 @@
import { customElement } from "lit-element"; import { customElement } from "lit-element";
import { import {
DeviceAction, DeviceAction,
fetchDeviceActions,
localizeDeviceAutomationAction, localizeDeviceAutomationAction,
} from "../../../../data/device_automation"; } from "../../../../data/device_automation";
@ -15,7 +14,7 @@ export class HaDeviceActionsCard extends HaDeviceAutomationCard<DeviceAction> {
protected headerKey = "ui.panel.config.devices.automation.actions.caption"; protected headerKey = "ui.panel.config.devices.automation.actions.caption";
constructor() { constructor() {
super(localizeDeviceAutomationAction, fetchDeviceActions); super(localizeDeviceAutomationAction);
} }
} }

View File

@ -11,34 +11,27 @@ export abstract class HaDeviceAutomationCard<
> extends LitElement { > extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property() public deviceId?: string; @property() public deviceId?: string;
@property() public automations: T[] = [];
protected headerKey = ""; protected headerKey = "";
protected type = ""; protected type = "";
@property() private _automations: T[] = [];
private _localizeDeviceAutomation: ( private _localizeDeviceAutomation: (
hass: HomeAssistant, hass: HomeAssistant,
automation: T automation: T
) => string; ) => string;
private _fetchDeviceAutomations: (
hass: HomeAssistant,
deviceId: string
) => Promise<T[]>;
constructor( constructor(
localizeDeviceAutomation: HaDeviceAutomationCard< localizeDeviceAutomation: HaDeviceAutomationCard<
T T
>["_localizeDeviceAutomation"], >["_localizeDeviceAutomation"]
fetchDeviceAutomations: HaDeviceAutomationCard<T>["_fetchDeviceAutomations"]
) { ) {
super(); super();
this._localizeDeviceAutomation = localizeDeviceAutomation; this._localizeDeviceAutomation = localizeDeviceAutomation;
this._fetchDeviceAutomations = fetchDeviceAutomations;
} }
protected shouldUpdate(changedProps): boolean { protected shouldUpdate(changedProps): boolean {
if (changedProps.has("deviceId") || changedProps.has("_automations")) { if (changedProps.has("deviceId") || changedProps.has("automations")) {
return true; return true;
} }
const oldHass = changedProps.get("hass"); const oldHass = changedProps.get("hass");
@ -48,18 +41,8 @@ export abstract class HaDeviceAutomationCard<
return false; return false;
} }
protected async updated(changedProps): Promise<void> {
super.updated(changedProps);
if (changedProps.has("deviceId")) {
this._automations = this.deviceId
? await this._fetchDeviceAutomations(this.hass, this.deviceId)
: [];
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (this._automations.length === 0) { if (this.automations.length === 0) {
return html``; return html``;
} }
return html` return html`
@ -70,7 +53,7 @@ export abstract class HaDeviceAutomationCard<
<div class="card-content"> <div class="card-content">
<ha-chips <ha-chips
@chip-clicked=${this._handleAutomationClicked} @chip-clicked=${this._handleAutomationClicked}
.items=${this._automations.map((automation) => .items=${this.automations.map((automation) =>
this._localizeDeviceAutomation(this.hass, automation) this._localizeDeviceAutomation(this.hass, automation)
)} )}
> >
@ -81,7 +64,7 @@ export abstract class HaDeviceAutomationCard<
} }
private _handleAutomationClicked(ev: CustomEvent) { private _handleAutomationClicked(ev: CustomEvent) {
const automation = this._automations[ev.detail.index]; const automation = this.automations[ev.detail.index];
if (!automation) { if (!automation) {
return; return;
} }

View File

@ -1,7 +1,6 @@
import { customElement } from "lit-element"; import { customElement } from "lit-element";
import { import {
DeviceCondition, DeviceCondition,
fetchDeviceConditions,
localizeDeviceAutomationCondition, localizeDeviceAutomationCondition,
} from "../../../../data/device_automation"; } from "../../../../data/device_automation";
@ -17,7 +16,7 @@ export class HaDeviceConditionsCard extends HaDeviceAutomationCard<
protected headerKey = "ui.panel.config.devices.automation.conditions.caption"; protected headerKey = "ui.panel.config.devices.automation.conditions.caption";
constructor() { constructor() {
super(localizeDeviceAutomationCondition, fetchDeviceConditions); super(localizeDeviceAutomationCondition);
} }
} }

View File

@ -10,9 +10,7 @@ import {
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import memoizeOne from "memoize-one";
import { compare } from "../../../../common/string/compare";
import "../../../../components/entity/state-badge"; import "../../../../components/entity/state-badge";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
@ -22,40 +20,23 @@ import "@polymer/paper-item/paper-item-body";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-icon"; import "../../../../components/ha-icon";
import "../../../../components/ha-switch"; import "../../../../components/ha-switch";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { showEntityRegistryDetailDialog } from "../../entity_registry/show-dialog-entity-registry-detail"; import { showEntityRegistryDetailDialog } from "../../entity_registry/show-dialog-entity-registry-detail";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain"; import { computeDomain } from "../../../../common/entity/compute_domain";
import { domainIcon } from "../../../../common/entity/domain_icon"; import { domainIcon } from "../../../../common/entity/domain_icon";
// tslint:disable-next-line // tslint:disable-next-line
import { HaSwitch } from "../../../../components/ha-switch"; import { HaSwitch } from "../../../../components/ha-switch";
import { EntityRegistryStateEntry } from "../ha-config-device-page";
@customElement("ha-device-entities-card") @customElement("ha-device-entities-card")
export class HaDeviceEntitiesCard extends LitElement { export class HaDeviceEntitiesCard extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property() public deviceId!: string; @property() public deviceId!: string;
@property() public entities!: EntityRegistryEntry[]; @property() public entities!: EntityRegistryStateEntry[];
@property() public narrow!: boolean; @property() public narrow!: boolean;
@property() private _showDisabled = false; @property() private _showDisabled = false;
private _entities = memoizeOne(
(
deviceId: string,
entities: EntityRegistryEntry[]
): EntityRegistryEntry[] =>
entities
.filter((entity) => entity.device_id === deviceId)
.sort((ent1, ent2) =>
compare(
this._computeEntityName(ent1) || `zzz${ent1.entity_id}`,
this._computeEntityName(ent2) || `zzz${ent2.entity_id}`
)
)
);
protected render(): TemplateResult { protected render(): TemplateResult {
const entities = this._entities(this.deviceId, this.entities);
return html` return html`
<ha-card> <ha-card>
<paper-item> <paper-item>
@ -67,8 +48,8 @@ export class HaDeviceEntitiesCard extends LitElement {
)} )}
</ha-switch> </ha-switch>
</paper-item> </paper-item>
${entities.length ${this.entities.length
? entities.map((entry: EntityRegistryEntry) => { ? this.entities.map((entry: EntityRegistryStateEntry) => {
if (!this._showDisabled && entry.disabled_by) { if (!this._showDisabled && entry.disabled_by) {
return ""; return "";
} }
@ -92,7 +73,7 @@ export class HaDeviceEntitiesCard extends LitElement {
></ha-icon> ></ha-icon>
`} `}
<paper-item-body two-line> <paper-item-body two-line>
<div class="name">${this._computeEntityName(entry)}</div> <div class="name">${entry.stateName}</div>
<div class="secondary entity-id">${entry.entity_id}</div> <div class="secondary entity-id">${entry.entity_id}</div>
</paper-item-body> </paper-item-body>
<div class="buttons"> <div class="buttons">
@ -143,14 +124,6 @@ export class HaDeviceEntitiesCard extends LitElement {
fireEvent(this, "hass-more-info", { entityId: entry.entity_id }); fireEvent(this, "hass-more-info", { entityId: entry.entity_id });
} }
private _computeEntityName(entity) {
if (entity.name) {
return entity.name;
}
const state = this.hass.states[entity.entity_id];
return state ? computeStateName(state) : null;
}
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
ha-icon { ha-icon {

View File

@ -1,7 +1,6 @@
import { customElement } from "lit-element"; import { customElement } from "lit-element";
import { import {
DeviceTrigger, DeviceTrigger,
fetchDeviceTriggers,
localizeDeviceAutomationTrigger, localizeDeviceAutomationTrigger,
} from "../../../../data/device_automation"; } from "../../../../data/device_automation";
@ -15,7 +14,7 @@ export class HaDeviceTriggersCard extends HaDeviceAutomationCard<
protected headerKey = "ui.panel.config.devices.automation.triggers.caption"; protected headerKey = "ui.panel.config.devices.automation.triggers.caption";
constructor() { constructor() {
super(localizeDeviceAutomationTrigger, fetchDeviceTriggers); super(localizeDeviceAutomationTrigger);
} }
} }

View File

@ -19,7 +19,10 @@ import "./device-detail/ha-device-actions-card";
import "./device-detail/ha-device-entities-card"; import "./device-detail/ha-device-entities-card";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { ConfigEntry } from "../../../data/config_entries"; import { ConfigEntry } from "../../../data/config_entries";
import { EntityRegistryEntry } from "../../../data/entity_registry"; import {
EntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { import {
DeviceRegistryEntry, DeviceRegistryEntry,
updateDeviceRegistryEntry, updateDeviceRegistryEntry,
@ -30,6 +33,22 @@ import {
showDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog,
} from "../../../dialogs/device-registry-detail/show-dialog-device-registry-detail"; } from "../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
import {
DeviceTrigger,
DeviceAction,
DeviceCondition,
fetchDeviceTriggers,
fetchDeviceConditions,
fetchDeviceActions,
} from "../../../data/device_automation";
import { compare } from "../../../common/string/compare";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { createValidEntityId } from "../../../common/entity/valid_entity_id";
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string;
}
@customElement("ha-config-device-page") @customElement("ha-config-device-page")
export class HaConfigDevicePage extends LitElement { export class HaConfigDevicePage extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@ -39,6 +58,10 @@ export class HaConfigDevicePage extends LitElement {
@property() public areas!: AreaRegistryEntry[]; @property() public areas!: AreaRegistryEntry[];
@property() public deviceId!: string; @property() public deviceId!: string;
@property() public narrow!: boolean; @property() public narrow!: boolean;
@property() public showAdvanced!: boolean;
@property() private _triggers: DeviceTrigger[] = [];
@property() private _conditions: DeviceCondition[] = [];
@property() private _actions: DeviceAction[] = [];
private _device = memoizeOne( private _device = memoizeOne(
( (
@ -48,11 +71,51 @@ export class HaConfigDevicePage extends LitElement {
devices ? devices.find((device) => device.id === deviceId) : undefined devices ? devices.find((device) => device.id === deviceId) : undefined
); );
private _entities = memoizeOne(
(
deviceId: string,
entities: EntityRegistryEntry[]
): EntityRegistryStateEntry[] =>
entities
.filter((entity) => entity.device_id === deviceId)
.map((entity) => {
return { ...entity, stateName: this._computeEntityName(entity) };
})
.sort((ent1, ent2) =>
compare(
ent1.stateName || `zzz${ent1.entity_id}`,
ent2.stateName || `zzz${ent2.entity_id}`
)
)
);
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
loadDeviceRegistryDetailDialog(); loadDeviceRegistryDetailDialog();
} }
protected updated(changedProps): void {
super.updated(changedProps);
if (changedProps.has("deviceId")) {
if (this.deviceId) {
fetchDeviceTriggers(this.hass, this.deviceId).then(
(triggers) => (this._triggers = triggers)
);
fetchDeviceConditions(this.hass, this.deviceId).then(
(conditions) => (this._conditions = conditions)
);
fetchDeviceActions(this.hass, this.deviceId).then(
(actions) => (this._actions = actions)
);
} else {
this._triggers = [];
this._conditions = [];
this._actions = [];
}
}
}
protected render() { protected render() {
const device = this._device(this.deviceId, this.devices); const device = this._device(this.deviceId, this.devices);
@ -62,6 +125,8 @@ export class HaConfigDevicePage extends LitElement {
`; `;
} }
const entities = this._entities(this.deviceId, this.entities);
return html` return html`
<hass-subpage .header=${device.name_by_user || device.name}> <hass-subpage .header=${device.name_by_user || device.name}>
<paper-icon-button <paper-icon-button
@ -84,37 +149,114 @@ export class HaConfigDevicePage extends LitElement {
hide-entities hide-entities
></ha-device-card> ></ha-device-card>
<div class="header">Entities</div> ${entities.length
<ha-device-entities-card ? html`
.hass=${this.hass} <div class="header">Entities</div>
.deviceId=${this.deviceId} <ha-device-entities-card
.entities=${this.entities} .hass=${this.hass}
> .entities=${entities}
</ha-device-entities-card> >
</ha-device-entities-card>
<div class="header">Automations</div> `
<ha-device-triggers-card : html``}
.hass=${this.hass} ${this._triggers.length ||
.deviceId=${this.deviceId} this._conditions.length ||
></ha-device-triggers-card> this._actions.length
<ha-device-conditions-card ? html`
.hass=${this.hass} <div class="header">Automations</div>
.deviceId=${this.deviceId} ${this._triggers.length
></ha-device-conditions-card> ? html`
<ha-device-actions-card <ha-device-triggers-card
.hass=${this.hass} .hass=${this.hass}
.deviceId=${this.deviceId} .automations=${this._triggers}
></ha-device-actions-card> ></ha-device-triggers-card>
`
: ""}
${this._conditions.length
? html`
<ha-device-conditions-card
.hass=${this.hass}
.automations=${this._conditions}
></ha-device-conditions-card>
`
: ""}
${this._actions.length
? html`
<ha-device-actions-card
.hass=${this.hass}
.automations=${this._actions}
></ha-device-actions-card>
`
: ""}
`
: html``}
</ha-config-section> </ha-config-section>
</hass-subpage> </hass-subpage>
`; `;
} }
private _showSettings() { private _computeEntityName(entity) {
if (entity.name) {
return entity.name;
}
const state = this.hass.states[entity.entity_id];
return state ? computeStateName(state) : null;
}
private async _showSettings() {
const device = this._device(this.deviceId, this.devices)!;
showDeviceRegistryDetailDialog(this, { showDeviceRegistryDetailDialog(this, {
device: this._device(this.deviceId, this.devices)!, device,
updateEntry: async (updates) => { updateEntry: async (updates) => {
const oldDeviceName = device.name_by_user || device.name;
const newDeviceName = updates.name_by_user;
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates); await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
if (
!oldDeviceName ||
!newDeviceName ||
oldDeviceName === newDeviceName
) {
return;
}
const entities = this._entities(this.deviceId, this.entities);
const renameEntityid =
this.showAdvanced &&
confirm(
"Do you also want to rename the entity id's of your entities?"
);
const updateProms = entities.map((entity) => {
const name = entity.name || entity.stateName;
let newEntityId: string | null = null;
let newName: string | null = null;
if (name && name.includes(oldDeviceName)) {
newName = name.replace(oldDeviceName, newDeviceName);
}
if (renameEntityid) {
const oldSearch = createValidEntityId(oldDeviceName);
if (entity.entity_id.includes(oldSearch)) {
newEntityId = entity.entity_id.replace(
oldSearch,
createValidEntityId(newDeviceName)
);
}
}
if (!newName && !newEntityId) {
return new Promise((resolve) => resolve());
}
return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
name: newName || name,
disabled_by: entity.disabled_by,
new_entity_id: newEntityId || entity.entity_id,
});
});
await Promise.all(updateProms);
}, },
}); });
} }

View File

@ -28,6 +28,7 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
class HaConfigDevices extends HassRouterPage { class HaConfigDevices extends HassRouterPage {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property() public narrow!: boolean; @property() public narrow!: boolean;
@property() public showAdvanced!: boolean;
protected routerOptions: RouterOptions = { protected routerOptions: RouterOptions = {
defaultPage: "dashboard", defaultPage: "dashboard",
@ -96,6 +97,7 @@ class HaConfigDevices extends HassRouterPage {
pageEl.devices = this._deviceRegistryEntries; pageEl.devices = this._deviceRegistryEntries;
pageEl.areas = this._areas; pageEl.areas = this._areas;
pageEl.narrow = this.narrow; pageEl.narrow = this.narrow;
pageEl.showAdvanced = this.showAdvanced;
} }
private _loadData() { private _loadData() {

View File

@ -15,7 +15,7 @@ import "@material/mwc-ripple";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../components/hui-warning"; import "../components/hui-warning";
import isValidEntityId from "../../../common/entity/valid_entity_id"; import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { stateIcon } from "../../../common/entity/state_icon"; import { stateIcon } from "../../../common/entity/state_icon";
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";

View File

@ -13,7 +13,7 @@ import { styleMap } from "lit-html/directives/style-map";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../components/hui-warning"; import "../components/hui-warning";
import isValidEntityId from "../../../common/entity/valid_entity_id"; import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element"; import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";

View File

@ -12,7 +12,7 @@ import {
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../components/hui-warning"; import "../components/hui-warning";
import isValidEntityId from "../../../common/entity/valid_entity_id"; import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";

View File

@ -1,5 +1,5 @@
// Parse array of entity objects from config // Parse array of entity objects from config
import isValidEntityId from "../../../common/entity/valid_entity_id"; import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { EntityConfig } from "../entity-rows/types"; import { EntityConfig } from "../entity-rows/types";
export const processConfigEntities = <T extends EntityConfig>( export const processConfigEntities = <T extends EntityConfig>(