diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index ac8b8ba5a9..c3c8222f3c 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -11,7 +11,7 @@ export interface LovelaceConfig { export interface LovelaceViewConfig { index?: number; title?: string; - badges?: string[]; + badges?: Array; cards?: LovelaceCardConfig[]; path?: string; icon?: string; @@ -25,6 +25,11 @@ export interface ShowViewConfig { user?: string; } +export interface LovelaceBadgeConfig { + type?: string; + [key: string]: any; +} + export interface LovelaceCardConfig { index?: number; view_index?: number; diff --git a/src/panels/lovelace/badges/hui-entity-filter-badge.ts b/src/panels/lovelace/badges/hui-entity-filter-badge.ts new file mode 100644 index 0000000000..eb569aafc9 --- /dev/null +++ b/src/panels/lovelace/badges/hui-entity-filter-badge.ts @@ -0,0 +1,140 @@ +import { createBadgeElement } from "../common/create-badge-element"; +import { processConfigEntities } from "../common/process-config-entities"; +import { LovelaceBadge } from "../types"; +import { EntityFilterEntityConfig } from "../entity-rows/types"; +import { HomeAssistant } from "../../../types"; +import { EntityFilterBadgeConfig } from "./types"; +import { evaluateFilter } from "../common/evaluate-filter"; + +class EntityFilterBadge extends HTMLElement implements LovelaceBadge { + private _elements?: LovelaceBadge[]; + private _config?: EntityFilterBadgeConfig; + private _configEntities?: EntityFilterEntityConfig[]; + private _hass?: HomeAssistant; + private _oldEntities?: EntityFilterEntityConfig[]; + + public setConfig(config: EntityFilterBadgeConfig): void { + if (!config.entities || !Array.isArray(config.entities)) { + throw new Error("entities must be specified."); + } + + if ( + !(config.state_filter && Array.isArray(config.state_filter)) && + !config.entities.every( + (entity) => + typeof entity === "object" && + entity.state_filter && + Array.isArray(entity.state_filter) + ) + ) { + throw new Error("Incorrect filter config."); + } + + this._config = config; + this._configEntities = undefined; + + if (this.lastChild) { + this.removeChild(this.lastChild); + this._elements = undefined; + } + } + + set hass(hass: HomeAssistant) { + if (!hass || !this._config) { + return; + } + + if (this._elements) { + for (const element of this._elements) { + element.hass = hass; + } + } + + if (!this.haveEntitiesChanged(hass)) { + this._hass = hass; + return; + } + + this._hass = hass; + + if (!this._configEntities) { + this._configEntities = processConfigEntities(this._config.entities); + } + + const entitiesList = this._configEntities.filter((entityConf) => { + const stateObj = hass.states[entityConf.entity]; + + if (!stateObj) { + return false; + } + + if (entityConf.state_filter) { + for (const filter of entityConf.state_filter) { + if (evaluateFilter(stateObj, filter)) { + return true; + } + } + } else { + for (const filter of this._config!.state_filter) { + if (evaluateFilter(stateObj, filter)) { + return true; + } + } + } + + return false; + }); + + if (entitiesList.length === 0) { + this.style.display = "none"; + return; + } + + const isSame = + this._oldEntities && + entitiesList.length === this._oldEntities.length && + entitiesList.every((entity, idx) => entity === this._oldEntities![idx]); + + if (!isSame) { + this._elements = []; + for (const badgeConfig of entitiesList) { + const element = createBadgeElement(badgeConfig); + element.hass = hass; + this._elements.push(element); + } + this._oldEntities = entitiesList; + } + + if (!this._elements) { + return; + } + + // Attach element if it has never been attached. + if (!this.lastChild) { + for (const element of this._elements) { + this.appendChild(element); + } + } + + this.style.display = "inline"; + } + + private haveEntitiesChanged(hass: HomeAssistant): boolean { + if (!this._hass) { + return true; + } + + if (!this._configEntities || this._hass.localize !== hass.localize) { + return true; + } + + for (const config of this._configEntities) { + if (this._hass.states[config.entity] !== hass.states[config.entity]) { + return true; + } + } + + return false; + } +} +customElements.define("hui-entity-filter-badge", EntityFilterBadge); diff --git a/src/panels/lovelace/badges/hui-error-badge.ts b/src/panels/lovelace/badges/hui-error-badge.ts new file mode 100644 index 0000000000..9c20ceff39 --- /dev/null +++ b/src/panels/lovelace/badges/hui-error-badge.ts @@ -0,0 +1,65 @@ +import { + html, + LitElement, + TemplateResult, + customElement, + property, + css, + CSSResult, +} from "lit-element"; + +import { LovelaceBadge } from "../types"; +import { HomeAssistant } from "../../../types"; +import { ErrorBadgeConfig } from "./types"; + +import "../../../components/ha-label-badge"; + +export const createErrorBadgeElement = (config) => { + const el = document.createElement("hui-error-badge"); + el.setConfig(config); + return el; +}; + +export const createErrorBadgeConfig = (error) => ({ + type: "error", + error, +}); + +@customElement("hui-error-badge") +export class HuiErrorBadge extends LitElement implements LovelaceBadge { + public hass?: HomeAssistant; + + @property() private _config?: ErrorBadgeConfig; + + public setConfig(config: ErrorBadgeConfig): void { + this._config = config; + } + + protected render(): TemplateResult | void { + if (!this._config) { + return html``; + } + + return html` + + `; + } + + static get styles(): CSSResult { + return css` + :host { + --ha-label-badge-color: var(--label-badge-red, #fce588); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-error-badge": HuiErrorBadge; + } +} diff --git a/src/panels/lovelace/badges/hui-state-label-badge.ts b/src/panels/lovelace/badges/hui-state-label-badge.ts new file mode 100644 index 0000000000..acef603a0a --- /dev/null +++ b/src/panels/lovelace/badges/hui-state-label-badge.ts @@ -0,0 +1,53 @@ +import { + html, + LitElement, + TemplateResult, + customElement, + property, +} from "lit-element"; + +import "../../../components/entity/ha-state-label-badge"; +import "../components/hui-warning-element"; + +import { LovelaceBadge } from "../types"; +import { HomeAssistant } from "../../../types"; +import { computeStateName } from "../../../common/entity/compute_state_name"; +import { StateLabelBadgeConfig } from "./types"; + +@customElement("hui-state-label-badge") +export class HuiStateLabelBadge extends LitElement implements LovelaceBadge { + @property() public hass?: HomeAssistant; + @property() protected _config?: StateLabelBadgeConfig; + + public setConfig(config: StateLabelBadgeConfig): void { + this._config = config; + } + + protected render(): TemplateResult | void { + if (!this._config || !this.hass) { + return html``; + } + + const stateObj = this.hass.states[this._config.entity!]; + + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-state-label-badge": HuiStateLabelBadge; + } +} diff --git a/src/panels/lovelace/badges/types.ts b/src/panels/lovelace/badges/types.ts new file mode 100644 index 0000000000..e462a0a4ee --- /dev/null +++ b/src/panels/lovelace/badges/types.ts @@ -0,0 +1,19 @@ +import { LovelaceBadgeConfig } from "../../../data/lovelace"; +import { EntityFilterEntityConfig } from "../entity-rows/types"; + +export interface EntityFilterBadgeConfig extends LovelaceBadgeConfig { + type: "entity-filter"; + entities: Array; + state_filter: Array<{ key: string } | string>; +} + +export interface ErrorBadgeConfig extends LovelaceBadgeConfig { + error: string; +} + +export interface StateLabelBadgeConfig extends LovelaceBadgeConfig { + entity: string; + name?: string; + icon?: string; + image?: string; +} diff --git a/src/panels/lovelace/common/create-badge-element.ts b/src/panels/lovelace/common/create-badge-element.ts new file mode 100644 index 0000000000..9a046cd0a5 --- /dev/null +++ b/src/panels/lovelace/common/create-badge-element.ts @@ -0,0 +1,73 @@ +import "../badges/hui-entity-filter-badge"; +import "../badges/hui-state-label-badge"; + +import { + createErrorBadgeElement, + createErrorBadgeConfig, + HuiErrorBadge, +} from "../badges/hui-error-badge"; +import { LovelaceBadge } from "../types"; +import { LovelaceBadgeConfig } from "../../../data/lovelace"; +import { fireEvent } from "../../../common/dom/fire_event"; + +const BADGE_TYPES = new Set(["entity-filter", "error", "state-label"]); +const CUSTOM_TYPE_PREFIX = "custom:"; +const TIMEOUT = 2000; + +const _createElement = ( + tag: string, + config: LovelaceBadgeConfig +): LovelaceBadge => { + const element = document.createElement(tag) as LovelaceBadge; + try { + element.setConfig(config); + } catch (err) { + // tslint:disable-next-line + console.error(tag, err); + return _createErrorElement(err.message); + } + return element; +}; + +const _createErrorElement = (error: string): HuiErrorBadge => + createErrorBadgeElement(createErrorBadgeConfig(error)); + +export const createBadgeElement = ( + config: LovelaceBadgeConfig +): LovelaceBadge => { + if (!config || typeof config !== "object") { + return _createErrorElement("No config"); + } + + let type = config.type; + + if (!type) { + type = "state-label"; + } + + if (type.startsWith(CUSTOM_TYPE_PREFIX)) { + const tag = type.substr(CUSTOM_TYPE_PREFIX.length); + + if (customElements.get(tag)) { + return _createElement(tag, config); + } + const element = _createErrorElement(`Type doesn't exist: ${tag}`); + element.style.display = "None"; + const timer = window.setTimeout(() => { + element.style.display = ""; + }, TIMEOUT); + + customElements.whenDefined(tag).then(() => { + clearTimeout(timer); + fireEvent(element, "ll-badge-rebuild"); + }); + + return element; + } + + if (!BADGE_TYPES.has(type)) { + return _createErrorElement(`Unknown type: ${type}`); + } + + return _createElement(`hui-${type}-badge`, config); +}; diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index de86979f83..3f1a3da723 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -34,6 +34,7 @@ import { subscribeEntityRegistry, EntityRegistryEntry, } from "../../../data/entity_registry"; +import { processEditorEntities } from "../editor/process-editor-entities"; const DEFAULT_VIEW_ENTITY_ID = "group.default_view"; const DOMAINS_BADGES = [ @@ -315,7 +316,7 @@ const generateViewConfig = ( const view: LovelaceViewConfig = { path, title, - badges, + badges: processEditorEntities(badges), cards, }; diff --git a/src/panels/lovelace/editor/view-editor/hui-edit-view.ts b/src/panels/lovelace/editor/view-editor/hui-edit-view.ts index ab7a051eb8..9d05bef13d 100644 --- a/src/panels/lovelace/editor/view-editor/hui-edit-view.ts +++ b/src/panels/lovelace/editor/view-editor/hui-edit-view.ts @@ -26,11 +26,11 @@ import { HomeAssistant } from "../../../../types"; import { LovelaceViewConfig, LovelaceCardConfig, + LovelaceBadgeConfig, } from "../../../../data/lovelace"; import { fireEvent } from "../../../../common/dom/fire_event"; import { EntitiesEditorEvent, ViewEditEvent } from "../types"; import { processEditorEntities } from "../process-editor-entities"; -import { EntityConfig } from "../../entity-rows/types"; import { navigate } from "../../../../common/navigate"; import { Lovelace } from "../../types"; import { deleteView, addView, replaceView } from "../config-util"; @@ -45,7 +45,7 @@ export class HuiEditView extends LitElement { @property() private _config?: LovelaceViewConfig; - @property() private _badges?: EntityConfig[]; + @property() private _badges?: LovelaceBadgeConfig[]; @property() private _cards?: LovelaceCardConfig[]; @@ -216,7 +216,7 @@ export class HuiEditView extends LitElement { const viewConf: LovelaceViewConfig = { ...this._config, - badges: this._badges!.map((entityConf) => entityConf.entity), + badges: this._badges, cards: this._cards, }; @@ -246,7 +246,7 @@ export class HuiEditView extends LitElement { if (!this._badges || !this.hass || !ev.detail || !ev.detail.entities) { return; } - this._badges = ev.detail.entities; + this._badges = processEditorEntities(ev.detail.entities); } private _isConfigChanged(): boolean { diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index 85f36b1c33..4557561412 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -1,10 +1,15 @@ import { HomeAssistant } from "../../types"; -import { LovelaceCardConfig, LovelaceConfig } from "../../data/lovelace"; +import { + LovelaceCardConfig, + LovelaceConfig, + LovelaceBadgeConfig, +} from "../../data/lovelace"; declare global { // tslint:disable-next-line interface HASSDomEvents { "ll-rebuild": {}; + "ll-badge-rebuild": {}; } } @@ -18,6 +23,11 @@ export interface Lovelace { saveConfig: (newConfig: LovelaceConfig) => Promise; } +export interface LovelaceBadge extends HTMLElement { + hass?: HomeAssistant; + setConfig(config: LovelaceBadgeConfig): void; +} + export interface LovelaceCard extends HTMLElement { hass?: HomeAssistant; isPanel?: boolean; diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts index 2bae8370be..08775f8809 100644 --- a/src/panels/lovelace/views/hui-view.ts +++ b/src/panels/lovelace/views/hui-view.ts @@ -8,21 +8,23 @@ import { import "../../../components/entity/ha-state-label-badge"; // This one is for types -// tslint:disable-next-line -import { HaStateLabelBadge } from "../../../components/entity/ha-state-label-badge"; import applyThemesOnElement from "../../../common/dom/apply_themes_on_element"; -import { LovelaceViewConfig, LovelaceCardConfig } from "../../../data/lovelace"; +import { + LovelaceViewConfig, + LovelaceCardConfig, + LovelaceBadgeConfig, +} from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; import { classMap } from "lit-html/directives/class-map"; -import { Lovelace, LovelaceCard } from "../types"; +import { Lovelace, LovelaceCard, LovelaceBadge } from "../types"; import { createCardElement } from "../common/create-card-element"; import { computeCardSize } from "../common/compute-card-size"; import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; import { HuiErrorCard } from "../cards/hui-error-card"; - import { computeRTL } from "../../../common/util/compute_rtl"; +import { createBadgeElement } from "../common/create-badge-element"; import { processConfigEntities } from "../common/process-config-entities"; let editCodeLoaded = false; @@ -51,7 +53,7 @@ export class HUIView extends LitElement { public columns?: number; public index?: number; private _cards: Array; - private _badges: Array<{ element: HaStateLabelBadge; entityId: string }>; + private _badges: LovelaceBadge[]; static get properties(): PropertyDeclarations { return { @@ -88,6 +90,19 @@ export class HUIView extends LitElement { return element; } + public createBadgeElement(badgeConfig: LovelaceBadgeConfig) { + const element = createBadgeElement(badgeConfig) as LovelaceBadge; + element.hass = this.hass; + element.addEventListener( + "ll-badge-rebuild", + () => { + this._rebuildBadge(element, badgeConfig); + }, + { once: true } + ); + return element; + } + protected render(): TemplateResult | void { return html` ${this.renderStyles()} @@ -208,9 +223,7 @@ export class HUIView extends LitElement { this._createBadges(lovelace.config.views[this.index!]); } else if (hassChanged) { this._badges.forEach((badge) => { - const { element, entityId } = badge; - element.hass = hass; - element.state = hass.states[entityId]; + badge.hass = hass; }); } @@ -261,16 +274,11 @@ export class HUIView extends LitElement { } const elements: HUIView["_badges"] = []; - const badges = processConfigEntities(config.badges); + const badges = processConfigEntities(config.badges as any); for (const badge of badges) { - const element = document.createElement("ha-state-label-badge"); - const entityId = badge.entity; + const element = createBadgeElement(badge); element.hass = this.hass; - element.state = this.hass!.states[entityId]; - element.name = badge.name; - element.icon = badge.icon; - element.image = badge.image; - elements.push({ element, entityId }); + elements.push(element); root.appendChild(element); } this._badges = elements; @@ -346,6 +354,17 @@ export class HUIView extends LitElement { curCardEl === cardElToReplace ? newCardEl : curCardEl ); } + + private _rebuildBadge( + badgeElToReplace: LovelaceBadge, + config: LovelaceBadgeConfig + ): void { + const newBadgeEl = this.createBadgeElement(config); + badgeElToReplace.parentElement!.replaceChild(newBadgeEl, badgeElToReplace); + this._badges = this._cards!.map((curBadgeEl) => + curBadgeEl === badgeElToReplace ? newBadgeEl : curBadgeEl + ); + } } declare global {