Skip to content
6 changes: 5 additions & 1 deletion src/data/lovelace.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { HomeAssistant } from "../types";
import { Connection, getCollection } from "home-assistant-js-websocket";
import { EntityConfig } from "../panels/lovelace/entity-rows/types";

export interface LovelaceConfig {
title?: string;
Expand All @@ -11,7 +12,7 @@ export interface LovelaceConfig {
export interface LovelaceViewConfig {
index?: number;
title?: string;
badges?: string[];
badges?: Array<string | LovelaceBadgeConfig>;
cards?: LovelaceCardConfig[];
path?: string;
icon?: string;
Expand All @@ -20,6 +21,9 @@ export interface LovelaceViewConfig {
background?: string;
}

// tslint:disable-next-line: no-empty-interface
Comment thread
iantrich marked this conversation as resolved.
Outdated
export interface LovelaceBadgeConfig extends EntityConfig {}
Comment thread
iantrich marked this conversation as resolved.
Outdated

export interface LovelaceCardConfig {
index?: number;
view_index?: number;
Expand Down
145 changes: 145 additions & 0 deletions src/panels/lovelace/badges/hui-entity-filter-badge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
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 {
public isPanel?: boolean;
Comment thread
iantrich marked this conversation as resolved.
Outdated
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.haveEntitiesChanged(hass)) {
this._hass = hass;
Comment thread
iantrich marked this conversation as resolved.

if (this._elements) {
for (const element of this._elements) {
element.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 = [];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to remove all the old elements ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You thinking we should keep a record of what is where and drop/add differences? Not sure if that's worth it

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haveEntitiesChanged could return an array of the entities that have changed, this._elements could be a Map with entity_id as key? Or is entity_id not unique enough?

for (const badgeConfig of entitiesList) {
const element = createBadgeElement(badgeConfig);
this._elements.push(element);
}
this._oldEntities = entitiesList;
}

if (!this._elements) {
return;
}

for (const element of this._elements) {
element.hass = hass;
Comment thread
iantrich marked this conversation as resolved.
Outdated
}

// Attach element if it has never been attached.
if (!this.lastChild) {
for (const element of this._elements) {
this.appendChild(element);
}
}

this.style.display = "inline";
Comment thread
iantrich marked this conversation as resolved.
}

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 changes: 65 additions & 0 deletions src/panels/lovelace/badges/hui-error-badge.ts
Original file line number Diff line number Diff line change
@@ -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-yellow, #fce588);
Comment thread
iantrich marked this conversation as resolved.
Outdated
}
`;
}
}

declare global {
interface HTMLElementTagNameMap {
"hui-error-badge": HuiErrorBadge;
}
}
55 changes: 55 additions & 0 deletions src/panels/lovelace/badges/hui-state-label-badge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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 { LovelaceBadgeConfig } from "../../../data/lovelace";

@customElement("hui-state-label-badge")
export class HuiStateLabelBadge extends LitElement implements LovelaceBadge {
@property() public hass?: HomeAssistant;
@property() protected _config?: LovelaceBadgeConfig;

public setConfig(config: LovelaceBadgeConfig): 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 === undefined
? stateObj
? computeStateName(stateObj)
: ""
: this._config.name === null
? ""
: this._config.name}
Comment thread
iantrich marked this conversation as resolved.
Outdated
.icon=${this._config.icon}
.image=${this._config.image}
></ha-state-label-badge>
`;
}
}

declare global {
interface HTMLElementTagNameMap {
"hui-state-label-badge": HuiStateLabelBadge;
}
}
12 changes: 12 additions & 0 deletions src/panels/lovelace/badges/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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;
}
73 changes: 73 additions & 0 deletions src/panels/lovelace/common/create-badge-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import deepClone from "deep-clone-simple";

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";

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 | HuiErrorBadge => {
Comment thread
iantrich marked this conversation as resolved.
Outdated
const element = document.createElement(tag) as LovelaceBadge;
try {
element.setConfig(deepClone(config));
Comment thread
iantrich marked this conversation as resolved.
Outdated
} 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 | HuiErrorBadge => {
Comment thread
iantrich marked this conversation as resolved.
Outdated
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);
Comment thread
iantrich marked this conversation as resolved.
});

return element;
}

if (!BADGE_TYPES.has(type)) {
return _createErrorElement(`Unknown type: ${type}`);
}

return _createElement(`hui-${type}-badge`, config);
};
3 changes: 2 additions & 1 deletion src/panels/lovelace/common/generate-lovelace-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -315,7 +316,7 @@ const generateViewConfig = (
const view: LovelaceViewConfig = {
path,
title,
badges,
badges: processEditorEntities(badges),
cards,
};

Expand Down
Loading