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+)$/;
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 { PolymerChangedEvent } from "../../polymer-types";
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";
// Not a duplicate, type import

View File

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

View File

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

View File

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

View File

@ -10,9 +10,7 @@ import {
import { classMap } from "lit-html/directives/class-map";
import { HomeAssistant } from "../../../../types";
import memoizeOne from "memoize-one";
import { compare } from "../../../../common/string/compare";
import "../../../../components/entity/state-badge";
import "@polymer/paper-item/paper-item";
@ -22,40 +20,23 @@ import "@polymer/paper-item/paper-item-body";
import "../../../../components/ha-card";
import "../../../../components/ha-icon";
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 { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { domainIcon } from "../../../../common/entity/domain_icon";
// tslint:disable-next-line
import { HaSwitch } from "../../../../components/ha-switch";
import { EntityRegistryStateEntry } from "../ha-config-device-page";
@customElement("ha-device-entities-card")
export class HaDeviceEntitiesCard extends LitElement {
@property() public hass!: HomeAssistant;
@property() public deviceId!: string;
@property() public entities!: EntityRegistryEntry[];
@property() public entities!: EntityRegistryStateEntry[];
@property() public narrow!: boolean;
@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 {
const entities = this._entities(this.deviceId, this.entities);
return html`
<ha-card>
<paper-item>
@ -67,8 +48,8 @@ export class HaDeviceEntitiesCard extends LitElement {
)}
</ha-switch>
</paper-item>
${entities.length
? entities.map((entry: EntityRegistryEntry) => {
${this.entities.length
? this.entities.map((entry: EntityRegistryStateEntry) => {
if (!this._showDisabled && entry.disabled_by) {
return "";
}
@ -92,7 +73,7 @@ export class HaDeviceEntitiesCard extends LitElement {
></ha-icon>
`}
<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>
</paper-item-body>
<div class="buttons">
@ -143,14 +124,6 @@ export class HaDeviceEntitiesCard extends LitElement {
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 {
return css`
ha-icon {

View File

@ -1,7 +1,6 @@
import { customElement } from "lit-element";
import {
DeviceTrigger,
fetchDeviceTriggers,
localizeDeviceAutomationTrigger,
} from "../../../../data/device_automation";
@ -15,7 +14,7 @@ export class HaDeviceTriggersCard extends HaDeviceAutomationCard<
protected headerKey = "ui.panel.config.devices.automation.triggers.caption";
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 { HomeAssistant } from "../../../types";
import { ConfigEntry } from "../../../data/config_entries";
import { EntityRegistryEntry } from "../../../data/entity_registry";
import {
EntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import {
DeviceRegistryEntry,
updateDeviceRegistryEntry,
@ -30,6 +33,22 @@ import {
showDeviceRegistryDetailDialog,
} 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")
export class HaConfigDevicePage extends LitElement {
@property() public hass!: HomeAssistant;
@ -39,6 +58,10 @@ export class HaConfigDevicePage extends LitElement {
@property() public areas!: AreaRegistryEntry[];
@property() public deviceId!: string;
@property() public narrow!: boolean;
@property() public showAdvanced!: boolean;
@property() private _triggers: DeviceTrigger[] = [];
@property() private _conditions: DeviceCondition[] = [];
@property() private _actions: DeviceAction[] = [];
private _device = memoizeOne(
(
@ -48,11 +71,51 @@ export class HaConfigDevicePage extends LitElement {
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) {
super.firstUpdated(changedProps);
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() {
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`
<hass-subpage .header=${device.name_by_user || device.name}>
<paper-icon-button
@ -84,37 +149,114 @@ export class HaConfigDevicePage extends LitElement {
hide-entities
></ha-device-card>
<div class="header">Entities</div>
<ha-device-entities-card
.hass=${this.hass}
.deviceId=${this.deviceId}
.entities=${this.entities}
>
</ha-device-entities-card>
<div class="header">Automations</div>
<ha-device-triggers-card
.hass=${this.hass}
.deviceId=${this.deviceId}
></ha-device-triggers-card>
<ha-device-conditions-card
.hass=${this.hass}
.deviceId=${this.deviceId}
></ha-device-conditions-card>
<ha-device-actions-card
.hass=${this.hass}
.deviceId=${this.deviceId}
></ha-device-actions-card>
${entities.length
? html`
<div class="header">Entities</div>
<ha-device-entities-card
.hass=${this.hass}
.entities=${entities}
>
</ha-device-entities-card>
`
: html``}
${this._triggers.length ||
this._conditions.length ||
this._actions.length
? html`
<div class="header">Automations</div>
${this._triggers.length
? html`
<ha-device-triggers-card
.hass=${this.hass}
.automations=${this._triggers}
></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>
</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, {
device: this._device(this.deviceId, this.devices)!,
device,
updateEntry: async (updates) => {
const oldDeviceName = device.name_by_user || device.name;
const newDeviceName = updates.name_by_user;
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 {
@property() public hass!: HomeAssistant;
@property() public narrow!: boolean;
@property() public showAdvanced!: boolean;
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
@ -96,6 +97,7 @@ class HaConfigDevices extends HassRouterPage {
pageEl.devices = this._deviceRegistryEntries;
pageEl.areas = this._areas;
pageEl.narrow = this.narrow;
pageEl.showAdvanced = this.showAdvanced;
}
private _loadData() {

View File

@ -15,7 +15,7 @@ import "@material/mwc-ripple";
import "../../../components/ha-card";
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 { computeStateDomain } from "../../../common/entity/compute_state_domain";
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/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 { computeStateName } from "../../../common/entity/compute_state_name";

View File

@ -12,7 +12,7 @@ import {
import "../../../components/ha-card";
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 { HomeAssistant } from "../../../types";

View File

@ -1,5 +1,5 @@
// 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";
export const processConfigEntities = <T extends EntityConfig>(