Custom badges (#3867)

* custom badges

* incremental

* functional

* cleanup

* cleanup

* address review comments

* address more comments

* address review comments

* address review comments

* cleanup

* address review comments

* address comments

* address comments

* fix entity-filter

* set hass once

* hass
This commit is contained in:
Ian Richardson 2019-10-16 09:09:13 -05:00 committed by Bram Kragten
parent df29a5becb
commit c9242a5075
10 changed files with 409 additions and 24 deletions

View File

@ -11,7 +11,7 @@ export interface LovelaceConfig {
export interface LovelaceViewConfig { export interface LovelaceViewConfig {
index?: number; index?: number;
title?: string; title?: string;
badges?: string[]; badges?: Array<string | LovelaceBadgeConfig>;
cards?: LovelaceCardConfig[]; cards?: LovelaceCardConfig[];
path?: string; path?: string;
icon?: string; icon?: string;
@ -25,6 +25,11 @@ export interface ShowViewConfig {
user?: string; user?: string;
} }
export interface LovelaceBadgeConfig {
type?: string;
[key: string]: any;
}
export interface LovelaceCardConfig { export interface LovelaceCardConfig {
index?: number; index?: number;
view_index?: number; view_index?: number;

View File

@ -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);

View File

@ -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`
<ha-label-badge
label="Error"
icon="hass:alert"
description=${this._config.error}
></ha-label-badge>
`;
}
static get styles(): CSSResult {
return css`
:host {
--ha-label-badge-color: var(--label-badge-red, #fce588);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-error-badge": HuiErrorBadge;
}
}

View File

@ -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`
<ha-state-label-badge
.hass=${this.hass}
.state=${stateObj}
.title=${this._config.name
? this._config.name
: stateObj
? computeStateName(stateObj)
: ""}
.icon=${this._config.icon}
.image=${this._config.image}
></ha-state-label-badge>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-state-label-badge": HuiStateLabelBadge;
}
}

View File

@ -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<EntityFilterEntityConfig | string>;
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;
}

View File

@ -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);
};

View File

@ -34,6 +34,7 @@ import {
subscribeEntityRegistry, subscribeEntityRegistry,
EntityRegistryEntry, EntityRegistryEntry,
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import { processEditorEntities } from "../editor/process-editor-entities";
const DEFAULT_VIEW_ENTITY_ID = "group.default_view"; const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
const DOMAINS_BADGES = [ const DOMAINS_BADGES = [
@ -315,7 +316,7 @@ const generateViewConfig = (
const view: LovelaceViewConfig = { const view: LovelaceViewConfig = {
path, path,
title, title,
badges, badges: processEditorEntities(badges),
cards, cards,
}; };

View File

@ -26,11 +26,11 @@ import { HomeAssistant } from "../../../../types";
import { import {
LovelaceViewConfig, LovelaceViewConfig,
LovelaceCardConfig, LovelaceCardConfig,
LovelaceBadgeConfig,
} from "../../../../data/lovelace"; } from "../../../../data/lovelace";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { EntitiesEditorEvent, ViewEditEvent } from "../types"; import { EntitiesEditorEvent, ViewEditEvent } from "../types";
import { processEditorEntities } from "../process-editor-entities"; import { processEditorEntities } from "../process-editor-entities";
import { EntityConfig } from "../../entity-rows/types";
import { navigate } from "../../../../common/navigate"; import { navigate } from "../../../../common/navigate";
import { Lovelace } from "../../types"; import { Lovelace } from "../../types";
import { deleteView, addView, replaceView } from "../config-util"; import { deleteView, addView, replaceView } from "../config-util";
@ -45,7 +45,7 @@ export class HuiEditView extends LitElement {
@property() private _config?: LovelaceViewConfig; @property() private _config?: LovelaceViewConfig;
@property() private _badges?: EntityConfig[]; @property() private _badges?: LovelaceBadgeConfig[];
@property() private _cards?: LovelaceCardConfig[]; @property() private _cards?: LovelaceCardConfig[];
@ -216,7 +216,7 @@ export class HuiEditView extends LitElement {
const viewConf: LovelaceViewConfig = { const viewConf: LovelaceViewConfig = {
...this._config, ...this._config,
badges: this._badges!.map((entityConf) => entityConf.entity), badges: this._badges,
cards: this._cards, cards: this._cards,
}; };
@ -246,7 +246,7 @@ export class HuiEditView extends LitElement {
if (!this._badges || !this.hass || !ev.detail || !ev.detail.entities) { if (!this._badges || !this.hass || !ev.detail || !ev.detail.entities) {
return; return;
} }
this._badges = ev.detail.entities; this._badges = processEditorEntities(ev.detail.entities);
} }
private _isConfigChanged(): boolean { private _isConfigChanged(): boolean {

View File

@ -1,10 +1,15 @@
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { LovelaceCardConfig, LovelaceConfig } from "../../data/lovelace"; import {
LovelaceCardConfig,
LovelaceConfig,
LovelaceBadgeConfig,
} from "../../data/lovelace";
declare global { declare global {
// tslint:disable-next-line // tslint:disable-next-line
interface HASSDomEvents { interface HASSDomEvents {
"ll-rebuild": {}; "ll-rebuild": {};
"ll-badge-rebuild": {};
} }
} }
@ -18,6 +23,11 @@ export interface Lovelace {
saveConfig: (newConfig: LovelaceConfig) => Promise<void>; saveConfig: (newConfig: LovelaceConfig) => Promise<void>;
} }
export interface LovelaceBadge extends HTMLElement {
hass?: HomeAssistant;
setConfig(config: LovelaceBadgeConfig): void;
}
export interface LovelaceCard extends HTMLElement { export interface LovelaceCard extends HTMLElement {
hass?: HomeAssistant; hass?: HomeAssistant;
isPanel?: boolean; isPanel?: boolean;

View File

@ -8,21 +8,23 @@ import {
import "../../../components/entity/ha-state-label-badge"; import "../../../components/entity/ha-state-label-badge";
// This one is for types // 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 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 { HomeAssistant } from "../../../types";
import { classMap } from "lit-html/directives/class-map"; 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 { createCardElement } from "../common/create-card-element";
import { computeCardSize } from "../common/compute-card-size"; import { computeCardSize } from "../common/compute-card-size";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import { HuiErrorCard } from "../cards/hui-error-card"; import { HuiErrorCard } from "../cards/hui-error-card";
import { computeRTL } from "../../../common/util/compute_rtl"; import { computeRTL } from "../../../common/util/compute_rtl";
import { createBadgeElement } from "../common/create-badge-element";
import { processConfigEntities } from "../common/process-config-entities"; import { processConfigEntities } from "../common/process-config-entities";
let editCodeLoaded = false; let editCodeLoaded = false;
@ -51,7 +53,7 @@ export class HUIView extends LitElement {
public columns?: number; public columns?: number;
public index?: number; public index?: number;
private _cards: Array<LovelaceCard | HuiErrorCard>; private _cards: Array<LovelaceCard | HuiErrorCard>;
private _badges: Array<{ element: HaStateLabelBadge; entityId: string }>; private _badges: LovelaceBadge[];
static get properties(): PropertyDeclarations { static get properties(): PropertyDeclarations {
return { return {
@ -88,6 +90,19 @@ export class HUIView extends LitElement {
return element; 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 { protected render(): TemplateResult | void {
return html` return html`
${this.renderStyles()} ${this.renderStyles()}
@ -208,9 +223,7 @@ export class HUIView extends LitElement {
this._createBadges(lovelace.config.views[this.index!]); this._createBadges(lovelace.config.views[this.index!]);
} else if (hassChanged) { } else if (hassChanged) {
this._badges.forEach((badge) => { this._badges.forEach((badge) => {
const { element, entityId } = badge; badge.hass = hass;
element.hass = hass;
element.state = hass.states[entityId];
}); });
} }
@ -261,16 +274,11 @@ export class HUIView extends LitElement {
} }
const elements: HUIView["_badges"] = []; const elements: HUIView["_badges"] = [];
const badges = processConfigEntities(config.badges); const badges = processConfigEntities(config.badges as any);
for (const badge of badges) { for (const badge of badges) {
const element = document.createElement("ha-state-label-badge"); const element = createBadgeElement(badge);
const entityId = badge.entity;
element.hass = this.hass; element.hass = this.hass;
element.state = this.hass!.states[entityId]; elements.push(element);
element.name = badge.name;
element.icon = badge.icon;
element.image = badge.image;
elements.push({ element, entityId });
root.appendChild(element); root.appendChild(element);
} }
this._badges = elements; this._badges = elements;
@ -346,6 +354,17 @@ export class HUIView extends LitElement {
curCardEl === cardElToReplace ? newCardEl : curCardEl 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 { declare global {