mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 03:36:44 +00:00
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:
parent
df29a5becb
commit
c9242a5075
@ -11,7 +11,7 @@ export interface LovelaceConfig {
|
||||
export interface LovelaceViewConfig {
|
||||
index?: number;
|
||||
title?: string;
|
||||
badges?: string[];
|
||||
badges?: Array<string | LovelaceBadgeConfig>;
|
||||
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;
|
||||
|
140
src/panels/lovelace/badges/hui-entity-filter-badge.ts
Normal file
140
src/panels/lovelace/badges/hui-entity-filter-badge.ts
Normal 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);
|
65
src/panels/lovelace/badges/hui-error-badge.ts
Normal file
65
src/panels/lovelace/badges/hui-error-badge.ts
Normal 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;
|
||||
}
|
||||
}
|
53
src/panels/lovelace/badges/hui-state-label-badge.ts
Normal file
53
src/panels/lovelace/badges/hui-state-label-badge.ts
Normal 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;
|
||||
}
|
||||
}
|
19
src/panels/lovelace/badges/types.ts
Normal file
19
src/panels/lovelace/badges/types.ts
Normal 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;
|
||||
}
|
73
src/panels/lovelace/common/create-badge-element.ts
Normal file
73
src/panels/lovelace/common/create-badge-element.ts
Normal 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);
|
||||
};
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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<void>;
|
||||
}
|
||||
|
||||
export interface LovelaceBadge extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
setConfig(config: LovelaceBadgeConfig): void;
|
||||
}
|
||||
|
||||
export interface LovelaceCard extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
isPanel?: boolean;
|
||||
|
@ -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<LovelaceCard | HuiErrorCard>;
|
||||
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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user