@@ -91,17 +104,17 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
: nothing}
${actionable ? html`` : nothing}
- ${this._config.entities?.length
+ ${badges?.length
? html`
-
- ${this._config.entities.map(
+
+ ${badges.map(
(config) => html`
-
-
+
`
)}
@@ -150,7 +163,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
.container .content:not(:has(p)) {
min-width: fit-content;
}
- .container .entities {
+ .container .badges {
flex: 0 0;
}
.content {
@@ -186,7 +199,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
font-weight: 500;
line-height: 20px;
}
- .entities {
+ .badges {
display: flex;
flex-direction: row;
align-items: center;
diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts
index e3d3257346..3c21617dfe 100644
--- a/src/panels/lovelace/cards/types.ts
+++ b/src/panels/lovelace/cards/types.ts
@@ -16,6 +16,7 @@ import {
LovelaceRowConfig,
} from "../entity-rows/types";
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
+import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
export type AlarmPanelCardConfigState =
| "arm_away"
@@ -503,21 +504,12 @@ export interface TileCardConfig extends LovelaceCardConfig {
features?: LovelaceCardFeatureConfig[];
}
-export interface HeadingEntityConfig {
- entity: string;
- state_content?: string | string[];
- icon?: string;
- show_state?: boolean;
- show_icon?: boolean;
- color?: string;
- tap_action?: ActionConfig;
- visibility?: Condition[];
-}
-
export interface HeadingCardConfig extends LovelaceCardConfig {
heading_style?: "title" | "subtitle";
heading?: string;
icon?: string;
tap_action?: ActionConfig;
- entities?: (string | HeadingEntityConfig)[];
+ badges?: LovelaceHeadingBadgeConfig[];
+ /** @deprecated Use `badges` instead */
+ entities?: LovelaceHeadingBadgeConfig[];
}
diff --git a/src/panels/lovelace/create-element/create-element-base.ts b/src/panels/lovelace/create-element/create-element-base.ts
index 33aaecb0af..7346e63b62 100644
--- a/src/panels/lovelace/create-element/create-element-base.ts
+++ b/src/panels/lovelace/create-element/create-element-base.ts
@@ -16,6 +16,7 @@ import type { ErrorCardConfig } from "../cards/types";
import { LovelaceElement, LovelaceElementConfig } from "../elements/types";
import { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types";
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
+import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
import {
LovelaceBadge,
LovelaceBadgeConstructor,
@@ -26,6 +27,8 @@ import {
LovelaceElementConstructor,
LovelaceHeaderFooter,
LovelaceHeaderFooterConstructor,
+ LovelaceHeadingBadge,
+ LovelaceHeadingBadgeConstructor,
LovelaceRowConstructor,
} from "../types";
@@ -72,6 +75,11 @@ interface CreateElementConfigTypes {
element: LovelaceSectionElement;
constructor: unknown;
};
+ "heading-badge": {
+ config: LovelaceHeadingBadgeConfig;
+ element: LovelaceHeadingBadge;
+ constructor: LovelaceHeadingBadgeConstructor;
+ };
}
export const createErrorCardElement = (config: ErrorCardConfig) => {
@@ -102,6 +110,20 @@ export const createErrorBadgeElement = (config: ErrorCardConfig) => {
return el;
};
+export const createErrorHeadingBadgeElement = (config: ErrorCardConfig) => {
+ const el = document.createElement("hui-error-heading-badge");
+ if (customElements.get("hui-error-heading-badge")) {
+ el.setConfig(config);
+ } else {
+ import("../heading-badges/hui-error-heading-badge");
+ customElements.whenDefined("hui-error-heading-badge").then(() => {
+ customElements.upgrade(el);
+ el.setConfig(config);
+ });
+ }
+ return el;
+};
+
export const createErrorCardConfig = (error, origConfig) => ({
type: "error",
error,
@@ -114,6 +136,12 @@ export const createErrorBadgeConfig = (error, origConfig) => ({
origConfig,
});
+export const createErrorHeadingBadgeConfig = (error, origConfig) => ({
+ type: "error",
+ error,
+ origConfig,
+});
+
const _createElement =
(
tag: string,
config: CreateElementConfigTypes[T]["config"]
@@ -134,6 +162,11 @@ const _createErrorElement = (
if (tagSuffix === "badge") {
return createErrorBadgeElement(createErrorBadgeConfig(error, config));
}
+ if (tagSuffix === "heading-badge") {
+ return createErrorHeadingBadgeElement(
+ createErrorHeadingBadgeConfig(error, config)
+ );
+ }
return createErrorCardElement(createErrorCardConfig(error, config));
};
diff --git a/src/panels/lovelace/create-element/create-heading-badge-element.ts b/src/panels/lovelace/create-element/create-heading-badge-element.ts
new file mode 100644
index 0000000000..e45bb6f14f
--- /dev/null
+++ b/src/panels/lovelace/create-element/create-heading-badge-element.ts
@@ -0,0 +1,22 @@
+import "../heading-badges/hui-entity-heading-badge";
+
+import {
+ createLovelaceElement,
+ getLovelaceElementClass,
+} from "./create-element-base";
+import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
+
+const ALWAYS_LOADED_TYPES = new Set(["error", "entity"]);
+
+export const createHeadingBadgeElement = (config: LovelaceHeadingBadgeConfig) =>
+ createLovelaceElement(
+ "heading-badge",
+ config,
+ ALWAYS_LOADED_TYPES,
+ undefined,
+ undefined,
+ "entity"
+ );
+
+export const getHeadingBadgeElementClass = (type: string) =>
+ getLovelaceElementClass(type, "heading-badge", ALWAYS_LOADED_TYPES);
diff --git a/src/panels/lovelace/editor/config-elements/hui-entities-editor.ts b/src/panels/lovelace/editor/config-elements/hui-heading-badges-editor.ts
similarity index 67%
rename from src/panels/lovelace/editor/config-elements/hui-entities-editor.ts
rename to src/panels/lovelace/editor/config-elements/hui-heading-badges-editor.ts
index e47abf3280..c102f3d37e 100644
--- a/src/panels/lovelace/editor/config-elements/hui-entities-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-heading-badges-editor.ts
@@ -14,23 +14,21 @@ import "../../../../components/ha-list-item";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import { HomeAssistant } from "../../../../types";
-
-type EntityConfig = {
- entity: string;
-};
+import { LovelaceHeadingBadgeConfig } from "../../heading-badges/types";
declare global {
interface HASSDomEvents {
- "edit-entity": { index: number };
+ "edit-heading-badge": { index: number };
+ "heading-badges-changed": { badges: LovelaceHeadingBadgeConfig[] };
}
}
-@customElement("hui-entities-editor")
-export class HuiEntitiesEditor extends LitElement {
+@customElement("hui-heading-badges-editor")
+export class HuiHeadingBadgesEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
- public entities?: EntityConfig[];
+ public badges?: LovelaceHeadingBadgeConfig[];
@query(".add-container", true) private _addContainer?: HTMLDivElement;
@@ -40,14 +38,30 @@ export class HuiEntitiesEditor extends LitElement {
private _opened = false;
- private _entitiesKeys = new WeakMap();
+ private _badgesKeys = new WeakMap();
- private _getKey(entity: EntityConfig) {
- if (!this._entitiesKeys.has(entity)) {
- this._entitiesKeys.set(entity, Math.random().toString());
+ private _getKey(badge: LovelaceHeadingBadgeConfig) {
+ if (!this._badgesKeys.has(badge)) {
+ this._badgesKeys.set(badge, Math.random().toString());
}
- return this._entitiesKeys.get(entity)!;
+ return this._badgesKeys.get(badge)!;
+ }
+
+ private _computeBadgeLabel(badge: LovelaceHeadingBadgeConfig) {
+ const type = badge.type ?? "entity";
+
+ if (type === "entity") {
+ const entityId = "entity" in badge ? (badge.entity as string) : undefined;
+ const stateObj = entityId ? this.hass.states[entityId] : undefined;
+ return (
+ (stateObj && stateObj.attributes.friendly_name) ||
+ entityId ||
+ type ||
+ "Unknown badge"
+ );
+ }
+ return type;
}
protected render() {
@@ -56,46 +70,35 @@ export class HuiEntitiesEditor extends LitElement {
}
return html`
- ${this.entities
+ ${this.badges
? html`
${repeat(
- this.entities,
- (entityConf) => this._getKey(entityConf),
- (entityConf, index) => {
- const editable = true;
-
- const entityId = entityConf.entity;
- const stateObj = this.hass.states[entityId];
- const name = stateObj
- ? stateObj.attributes.friendly_name
- : undefined;
+ this.badges,
+ (badge) => this._getKey(badge),
+ (badge, index) => {
+ const label = this._computeBadgeLabel(badge);
return html`
-
+
-
-
${name || entityId}
+
+ ${label}
- ${editable
- ? html`
-
- `
- : nothing}
+
* {
+ .badge .handle > * {
pointer-events: none;
}
- .entity-content {
+ .badge-content {
height: 60px;
font-size: 16px;
display: flex;
@@ -252,7 +258,7 @@ export class HuiEntitiesEditor extends LitElement {
flex-grow: 1;
}
- .entity-content div {
+ .badge-content div {
display: flex;
flex-direction: column;
}
@@ -291,6 +297,6 @@ export class HuiEntitiesEditor extends LitElement {
declare global {
interface HTMLElementTagNameMap {
- "hui-entities-editor": HuiEntitiesEditor;
+ "hui-heading-badges-editor": HuiHeadingBadgesEditor;
}
}
diff --git a/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts
index 727a4223d7..69c89e6bc8 100644
--- a/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts
@@ -22,15 +22,19 @@ import type {
} from "../../../../components/ha-form/types";
import "../../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../../types";
-import type { HeadingCardConfig, HeadingEntityConfig } from "../../cards/types";
+import { migrateHeadingCardConfig } from "../../cards/hui-heading-card";
+import type { HeadingCardConfig } from "../../cards/types";
import { UiAction } from "../../components/hui-action-editor";
+import {
+ EntityHeadingBadgeConfig,
+ LovelaceHeadingBadgeConfig,
+} from "../../heading-badges/types";
import type { LovelaceCardEditor } from "../../types";
-import { processEditorEntities } from "../process-editor-entities";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
-import { configElementStyle } from "./config-elements-style";
-import "./hui-entities-editor";
import { EditSubElementEvent } from "../types";
+import { configElementStyle } from "./config-elements-style";
+import "./hui-heading-badges-editor";
const actions: UiAction[] = ["navigate", "url", "perform-action", "none"];
@@ -41,7 +45,7 @@ const cardConfigStruct = assign(
heading: optional(string()),
icon: optional(string()),
tap_action: optional(actionConfigStruct),
- entities: optional(array(any())),
+ badges: optional(array(any())),
})
);
@@ -55,8 +59,8 @@ export class HuiHeadingCardEditor
@state() private _config?: HeadingCardConfig;
public setConfig(config: HeadingCardConfig): void {
- assert(config, cardConfigStruct);
- this._config = config;
+ this._config = migrateHeadingCardConfig(config);
+ assert(this._config, cardConfigStruct);
}
private _schema = memoizeOne(
@@ -103,8 +107,9 @@ export class HuiHeadingCardEditor
] as const satisfies readonly HaFormSchema[]
);
- private _entities = memoizeOne((entities: HeadingCardConfig["entities"]) =>
- processEditorEntities(entities || [])
+ private _badges = memoizeOne(
+ (badges: HeadingCardConfig["badges"]): LovelaceHeadingBadgeConfig[] =>
+ badges || []
);
protected render() {
@@ -138,19 +143,19 @@ export class HuiHeadingCardEditor
)}
-
-
+
`;
}
- private _entitiesChanged(ev: CustomEvent): void {
+ private _badgesChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
@@ -158,7 +163,7 @@ export class HuiHeadingCardEditor
const config = {
...this._config,
- entities: ev.detail.entities as HeadingEntityConfig[],
+ badges: ev.detail.badges as LovelaceHeadingBadgeConfig[],
};
fireEvent(this, "config-changed", { config });
@@ -175,22 +180,22 @@ export class HuiHeadingCardEditor
fireEvent(this, "config-changed", { config });
}
- private _editEntity(ev: HASSDomEvent<{ index: number }>): void {
+ private _editBadge(ev: HASSDomEvent<{ index: number }>): void {
ev.stopPropagation();
const index = ev.detail.index;
- const config = this._config!.entities![index!];
+ const config = this._badges(this._config!.badges)[index];
fireEvent(this, "edit-sub-element", {
config: config,
- saveConfig: (newConfig) => this._updateEntity(index, newConfig),
- type: "heading-entity",
- } as EditSubElementEvent);
+ saveConfig: (newConfig) => this._updateBadge(index, newConfig),
+ type: "heading-badge",
+ } as EditSubElementEvent);
}
- private _updateEntity(index: number, entity: HeadingEntityConfig) {
- const entities = this._config!.entities!.concat();
- entities[index] = entity;
- const config = { ...this._config!, entities };
+ private _updateBadge(index: number, entity: EntityHeadingBadgeConfig) {
+ const badges = this._config!.badges!.concat();
+ badges[index] = entity;
+ const config = { ...this._config!, badges };
fireEvent(this, "config-changed", {
config: config,
});
diff --git a/src/panels/lovelace/editor/heading-entity/hui-heading-entity-editor.ts b/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts
similarity index 92%
rename from src/panels/lovelace/editor/heading-entity/hui-heading-entity-editor.ts
rename to src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts
index 6d0c4f3d51..b1446d4a12 100644
--- a/src/panels/lovelace/editor/heading-entity/hui-heading-entity-editor.ts
+++ b/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts
@@ -21,19 +21,21 @@ import type {
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
-import type { HeadingEntityConfig } from "../../cards/types";
import { Condition } from "../../common/validate-condition";
+import { EntityHeadingBadgeConfig } from "../../heading-badges/types";
import type { LovelaceGenericElementEditor } from "../../types";
import "../conditions/ha-card-conditions-editor";
import { configElementStyle } from "../config-elements/config-elements-style";
import { actionConfigStruct } from "../structs/action-struct";
-export const DEFAULT_CONFIG: Partial = {
+export const DEFAULT_CONFIG: Partial = {
+ type: "entity",
show_state: true,
show_icon: true,
};
const entityConfigStruct = object({
+ type: optional(string()),
entity: string(),
icon: optional(string()),
state_content: optional(union([string(), array(string())])),
@@ -44,7 +46,7 @@ const entityConfigStruct = object({
visibility: optional(array(any())),
});
-type FormData = HeadingEntityConfig & {
+type FormData = EntityHeadingBadgeConfig & {
displayed_elements?: string[];
};
@@ -57,9 +59,9 @@ export class HuiHeadingEntityEditor
@property({ type: Boolean }) public preview = false;
- @state() private _config?: HeadingEntityConfig;
+ @state() private _config?: EntityHeadingBadgeConfig;
- public setConfig(config: HeadingEntityConfig): void {
+ public setConfig(config: EntityHeadingBadgeConfig): void {
assert(config, entityConfigStruct);
this._config = {
...DEFAULT_CONFIG,
@@ -150,12 +152,14 @@ export class HuiHeadingEntityEditor
] as const satisfies readonly HaFormSchema[]
);
- private _displayedElements = memoizeOne((config: HeadingEntityConfig) => {
- const elements: string[] = [];
- if (config.show_state) elements.push("state");
- if (config.show_icon) elements.push("icon");
- return elements;
- });
+ private _displayedElements = memoizeOne(
+ (config: EntityHeadingBadgeConfig) => {
+ const elements: string[] = [];
+ if (config.show_state) elements.push("state");
+ if (config.show_icon) elements.push("icon");
+ return elements;
+ }
+ );
protected render() {
if (!this.hass || !this._config) {
@@ -228,7 +232,7 @@ export class HuiHeadingEntityEditor
const conditions = ev.detail.value as Condition[];
- const newConfig: HeadingEntityConfig = {
+ const newConfig: EntityHeadingBadgeConfig = {
...this._config,
visibility: conditions,
};
diff --git a/src/panels/lovelace/editor/heading-badge-editor/hui-heading-badge-element-editor.ts b/src/panels/lovelace/editor/heading-badge-editor/hui-heading-badge-element-editor.ts
new file mode 100644
index 0000000000..dadef34f8b
--- /dev/null
+++ b/src/panels/lovelace/editor/heading-badge-editor/hui-heading-badge-element-editor.ts
@@ -0,0 +1,42 @@
+import { customElement } from "lit/decorators";
+import { getHeadingBadgeElementClass } from "../../create-element/create-heading-badge-element";
+import type { EntityHeadingBadgeConfig } from "../../heading-badges/types";
+import { LovelaceConfigForm, LovelaceHeadingBadgeEditor } from "../../types";
+import { HuiTypedElementEditor } from "../hui-typed-element-editor";
+
+@customElement("hui-heading-badge-element-editor")
+export class HuiHeadingEntityElementEditor extends HuiTypedElementEditor {
+ protected get configElementType(): string | undefined {
+ return this.value?.type || "entity";
+ }
+
+ protected async getConfigElement(): Promise<
+ LovelaceHeadingBadgeEditor | undefined
+ > {
+ const elClass = await getHeadingBadgeElementClass(this.configElementType!);
+
+ // Check if a GUI editor exists
+ if (elClass && elClass.getConfigElement) {
+ return elClass.getConfigElement();
+ }
+
+ return undefined;
+ }
+
+ protected async getConfigForm(): Promise {
+ const elClass = await getHeadingBadgeElementClass(this.configElementType!);
+
+ // Check if a schema exists
+ if (elClass && elClass.getConfigForm) {
+ return elClass.getConfigForm();
+ }
+
+ return undefined;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-heading-badge-element-editor": HuiHeadingEntityElementEditor;
+ }
+}
diff --git a/src/panels/lovelace/editor/heading-entity/hui-heading-entity-element-editor.ts b/src/panels/lovelace/editor/heading-entity/hui-heading-entity-element-editor.ts
deleted file mode 100644
index ada27a39d2..0000000000
--- a/src/panels/lovelace/editor/heading-entity/hui-heading-entity-element-editor.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { customElement } from "lit/decorators";
-import { HeadingEntityConfig } from "../../cards/types";
-import { HuiElementEditor } from "../hui-element-editor";
-import type { HuiHeadingEntityEditor } from "./hui-heading-entity-editor";
-
-@customElement("hui-heading-entity-element-editor")
-export class HuiHeadingEntityElementEditor extends HuiElementEditor {
- protected async getConfigElement(): Promise<
- HuiHeadingEntityEditor | undefined
- > {
- await import("./hui-heading-entity-editor");
- return document.createElement("hui-heading-entity-editor");
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- "hui-heading-entity-element-editor": HuiHeadingEntityElementEditor;
- }
-}
diff --git a/src/panels/lovelace/editor/hui-sub-element-editor.ts b/src/panels/lovelace/editor/hui-sub-element-editor.ts
index 1202d598a3..0d01341abc 100644
--- a/src/panels/lovelace/editor/hui-sub-element-editor.ts
+++ b/src/panels/lovelace/editor/hui-sub-element-editor.ts
@@ -15,7 +15,7 @@ import type { HomeAssistant } from "../../../types";
import "./entity-row-editor/hui-row-element-editor";
import "./feature-editor/hui-card-feature-element-editor";
import "./header-footer-editor/hui-header-footer-element-editor";
-import "./heading-entity/hui-heading-entity-element-editor";
+import "./heading-badge-editor/hui-heading-badge-element-editor";
import type { HuiElementEditor } from "./hui-element-editor";
import "./picture-element-editor/hui-picture-element-element-editor";
import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types";
@@ -132,16 +132,16 @@ export class HuiSubElementEditor extends LitElement {
@GUImode-changed=${this._handleGUIModeChanged}
>
`;
- case "heading-entity":
+ case "heading-badge":
return html`
-
+ >
`;
default:
return nothing;
diff --git a/src/panels/lovelace/editor/types.ts b/src/panels/lovelace/editor/types.ts
index a409f00729..c62c7595ab 100644
--- a/src/panels/lovelace/editor/types.ts
+++ b/src/panels/lovelace/editor/types.ts
@@ -9,7 +9,7 @@ import { LovelaceHeaderFooterConfig } from "../header-footer/types";
import { LovelaceCardFeatureConfig } from "../card-features/types";
import { LovelaceElementConfig } from "../elements/types";
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
-import { HeadingEntityConfig } from "../cards/types";
+import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
export interface YamlChangedEvent extends Event {
detail: {
@@ -97,10 +97,10 @@ export interface SubElementEditorConfig {
| LovelaceHeaderFooterConfig
| LovelaceCardFeatureConfig
| LovelaceElementConfig
- | HeadingEntityConfig;
+ | LovelaceHeadingBadgeConfig;
saveElementConfig?: (elementConfig: any) => void;
context?: any;
- type: "header" | "footer" | "row" | "feature" | "element" | "heading-entity";
+ type: "header" | "footer" | "row" | "feature" | "element" | "heading-badge";
}
export interface EditSubElementEvent {
diff --git a/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts b/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts
new file mode 100644
index 0000000000..b6084a846d
--- /dev/null
+++ b/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts
@@ -0,0 +1,177 @@
+import { mdiAlertCircle } from "@mdi/js";
+import { HassEntity } from "home-assistant-js-websocket";
+import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import { styleMap } from "lit/directives/style-map";
+import memoizeOne from "memoize-one";
+import { computeCssColor } from "../../../common/color/compute-color";
+import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
+import { computeDomain } from "../../../common/entity/compute_domain";
+import { stateActive } from "../../../common/entity/state_active";
+import { stateColorCss } from "../../../common/entity/state_color";
+import "../../../components/ha-heading-badge";
+import "../../../components/ha-state-icon";
+import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
+import "../../../state-display/state-display";
+import { HomeAssistant } from "../../../types";
+import { actionHandler } from "../common/directives/action-handler-directive";
+import { handleAction } from "../common/handle-action";
+import { hasAction } from "../common/has-action";
+import { DEFAULT_CONFIG } from "../editor/heading-badge-editor/hui-entity-heading-badge-editor";
+import { LovelaceHeadingBadge, LovelaceHeadingBadgeEditor } from "../types";
+import { EntityHeadingBadgeConfig } from "./types";
+
+@customElement("hui-entity-heading-badge")
+export class HuiEntityHeadingBadge
+ extends LitElement
+ implements LovelaceHeadingBadge
+{
+ public static async getConfigElement(): Promise {
+ await import(
+ "../editor/heading-badge-editor/hui-entity-heading-badge-editor"
+ );
+ return document.createElement("hui-heading-entity-editor");
+ }
+
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ @state() private _config?: EntityHeadingBadgeConfig;
+
+ @property({ type: Boolean }) public preview = false;
+
+ public setConfig(config): void {
+ this._config = {
+ ...DEFAULT_CONFIG,
+ tap_action: {
+ action: "none",
+ },
+ ...config,
+ };
+ }
+
+ private _handleAction(ev: ActionHandlerEvent) {
+ const config: EntityHeadingBadgeConfig = {
+ tap_action: {
+ action: "none",
+ },
+ ...this._config!,
+ };
+ handleAction(this, this.hass!, config, ev.detail.action!);
+ }
+
+ private _computeStateColor = memoizeOne(
+ (entity: HassEntity, color?: string) => {
+ if (!color || color === "none") {
+ return undefined;
+ }
+
+ if (color === "state") {
+ // Use light color if the light support rgb
+ if (
+ computeDomain(entity.entity_id) === "light" &&
+ entity.attributes.rgb_color
+ ) {
+ const hsvColor = rgb2hsv(entity.attributes.rgb_color);
+
+ // Modify the real rgb color for better contrast
+ if (hsvColor[1] < 0.4) {
+ // Special case for very light color (e.g: white)
+ if (hsvColor[1] < 0.1) {
+ hsvColor[2] = 225;
+ } else {
+ hsvColor[1] = 0.4;
+ }
+ }
+ return rgb2hex(hsv2rgb(hsvColor));
+ }
+ // Fallback to state color
+ return stateColorCss(entity);
+ }
+
+ if (color) {
+ // Use custom color if active
+ return stateActive(entity) ? computeCssColor(color) : undefined;
+ }
+ return color;
+ }
+ );
+
+ protected render() {
+ if (!this.hass || !this._config) {
+ return nothing;
+ }
+
+ const config = this._config;
+
+ const entityId = config.entity;
+ const stateObj = this.hass!.states[entityId];
+
+ if (!stateObj) {
+ return html`
+
+
+ -
+
+ `;
+ }
+
+ const color = this._computeStateColor(stateObj, config.color);
+
+ const style = {
+ "--icon-color": color,
+ };
+
+ return html`
+
+ ${config.show_icon
+ ? html`
+
+ `
+ : nothing}
+ ${config.show_state
+ ? html`
+
+ `
+ : nothing}
+
+ `;
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ [role="button"] {
+ cursor: pointer;
+ }
+ ha-heading-badge {
+ --state-inactive-color: initial;
+ }
+ ha-heading-badge.error {
+ --icon-color: var(--red-color);
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-entity-heading-badge": HuiEntityHeadingBadge;
+ }
+}
diff --git a/src/panels/lovelace/heading-badges/hui-error-heading-badge.ts b/src/panels/lovelace/heading-badges/hui-error-heading-badge.ts
new file mode 100644
index 0000000000..9210592763
--- /dev/null
+++ b/src/panels/lovelace/heading-badges/hui-error-heading-badge.ts
@@ -0,0 +1,94 @@
+import { mdiAlertCircle } from "@mdi/js";
+import { dump } from "js-yaml";
+import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
+import { customElement, state } from "lit/decorators";
+import "../../../components/ha-badge";
+import "../../../components/ha-svg-icon";
+import { HomeAssistant } from "../../../types";
+import { showAlertDialog } from "../custom-card-helpers";
+import { LovelaceBadge } from "../types";
+import { ErrorBadgeConfig } from "./types";
+
+export const createErrorHeadingBadgeElement = (config) => {
+ const el = document.createElement("hui-error-heading-badge");
+ el.setConfig(config);
+ return el;
+};
+
+export const createErrorHeadingBadgeConfig = (error) => ({
+ type: "error",
+ error,
+});
+
+@customElement("hui-error-heading-badge")
+export class HuiErrorHeadingBadge extends LitElement implements LovelaceBadge {
+ public hass?: HomeAssistant;
+
+ @state() private _config?: ErrorBadgeConfig;
+
+ public setConfig(config: ErrorBadgeConfig): void {
+ this._config = config;
+ }
+
+ private _viewDetail() {
+ let dumped: string | undefined;
+
+ if (this._config!.origConfig) {
+ try {
+ dumped = dump(this._config!.origConfig);
+ } catch (err: any) {
+ dumped = `[Error dumping ${this._config!.origConfig}]`;
+ }
+ }
+
+ showAlertDialog(this, {
+ title: this._config?.error,
+ warning: true,
+ text: dumped ? html`${dumped}
` : "",
+ });
+ }
+
+ protected render() {
+ if (!this._config) {
+ return nothing;
+ }
+
+ return html`
+
+
+ ${this._config.error}
+
+ `;
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ ha-heading-badge {
+ --icon-color: var(--error-color);
+ color: var(--error-color);
+ }
+ .content {
+ max-width: 70px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ pre {
+ font-family: var(--code-font-family, monospace);
+ white-space: break-spaces;
+ user-select: text;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-error-heading-badge": HuiErrorHeadingBadge;
+ }
+}
diff --git a/src/panels/lovelace/heading-badges/hui-heading-badge.ts b/src/panels/lovelace/heading-badges/hui-heading-badge.ts
new file mode 100644
index 0000000000..92c5b04ca8
--- /dev/null
+++ b/src/panels/lovelace/heading-badges/hui-heading-badge.ts
@@ -0,0 +1,202 @@
+import { PropertyValues, ReactiveElement } from "lit";
+import { customElement, property } from "lit/decorators";
+import { fireEvent } from "../../../common/dom/fire_event";
+import { MediaQueriesListener } from "../../../common/dom/media_query";
+import "../../../components/ha-svg-icon";
+import type { HomeAssistant } from "../../../types";
+import {
+ attachConditionMediaQueriesListeners,
+ checkConditionsMet,
+} from "../common/validate-condition";
+import { createHeadingBadgeElement } from "../create-element/create-heading-badge-element";
+import type { LovelaceHeadingBadge } from "../types";
+import { LovelaceHeadingBadgeConfig } from "./types";
+
+declare global {
+ interface HASSDomEvents {
+ "heading-badge-visibility-changed": { value: boolean };
+ "heading-badge-updated": undefined;
+ }
+}
+
+@customElement("hui-heading-badge")
+export class HuiHeadingBadge extends ReactiveElement {
+ @property({ type: Boolean }) public preview = false;
+
+ @property({ attribute: false }) public config?: LovelaceHeadingBadgeConfig;
+
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ private _elementConfig?: LovelaceHeadingBadgeConfig;
+
+ public load() {
+ if (!this.config) {
+ throw new Error("Cannot build heading badge without config");
+ }
+ this._loadElement(this.config);
+ }
+
+ private _element?: LovelaceHeadingBadge;
+
+ private _listeners: MediaQueriesListener[] = [];
+
+ protected createRenderRoot() {
+ return this;
+ }
+
+ public disconnectedCallback() {
+ super.disconnectedCallback();
+ this._clearMediaQueries();
+ }
+
+ public connectedCallback() {
+ super.connectedCallback();
+ this._listenMediaQueries();
+ this._updateVisibility();
+ }
+
+ private _updateElement(config: LovelaceHeadingBadgeConfig) {
+ if (!this._element) {
+ return;
+ }
+ this._element.setConfig(config);
+ this._elementConfig = config;
+ fireEvent(this, "heading-badge-updated");
+ }
+
+ private _loadElement(config: LovelaceHeadingBadgeConfig) {
+ this._element = createHeadingBadgeElement(config);
+ this._elementConfig = config;
+ if (this.hass) {
+ this._element.hass = this.hass;
+ }
+ this._element.addEventListener(
+ "ll-upgrade",
+ (ev: Event) => {
+ ev.stopPropagation();
+ if (this.hass) {
+ this._element!.hass = this.hass;
+ }
+ fireEvent(this, "heading-badge-updated");
+ },
+ { once: true }
+ );
+ this._element.addEventListener(
+ "ll-rebuild",
+ (ev: Event) => {
+ ev.stopPropagation();
+ this._loadElement(config);
+ fireEvent(this, "heading-badge-updated");
+ },
+ { once: true }
+ );
+ while (this.lastChild) {
+ this.removeChild(this.lastChild);
+ }
+ this._updateVisibility();
+ }
+
+ protected willUpdate(changedProps: PropertyValues): void {
+ super.willUpdate(changedProps);
+
+ if (!this._element) {
+ this.load();
+ }
+ }
+
+ protected update(changedProps: PropertyValues) {
+ super.update(changedProps);
+
+ if (this._element) {
+ if (changedProps.has("config")) {
+ const elementConfig = this._elementConfig;
+ if (this.config !== elementConfig && this.config) {
+ const typeChanged = this.config?.type !== elementConfig?.type;
+ if (typeChanged) {
+ this._loadElement(this.config);
+ } else {
+ this._updateElement(this.config);
+ }
+ }
+ }
+ if (changedProps.has("hass")) {
+ try {
+ if (this.hass) {
+ this._element.hass = this.hass;
+ }
+ } catch (e: any) {
+ this._element = undefined;
+ this._elementConfig = undefined;
+ }
+ }
+ }
+
+ if (changedProps.has("hass") || changedProps.has("preview")) {
+ this._updateVisibility();
+ }
+ }
+
+ private _clearMediaQueries() {
+ this._listeners.forEach((unsub) => unsub());
+ this._listeners = [];
+ }
+
+ private _listenMediaQueries() {
+ this._clearMediaQueries();
+ if (!this.config?.visibility) {
+ return;
+ }
+ const conditions = this.config.visibility;
+ const hasOnlyMediaQuery =
+ conditions.length === 1 &&
+ conditions[0].condition === "screen" &&
+ !!conditions[0].media_query;
+
+ this._listeners = attachConditionMediaQueriesListeners(
+ this.config.visibility,
+ (matches) => {
+ this._updateVisibility(hasOnlyMediaQuery && matches);
+ }
+ );
+ }
+
+ private _updateVisibility(forceVisible?: boolean) {
+ if (!this._element || !this.hass) {
+ return;
+ }
+
+ if (this._element.hidden) {
+ this._setElementVisibility(false);
+ return;
+ }
+
+ const visible =
+ forceVisible ||
+ this.preview ||
+ !this.config?.visibility ||
+ checkConditionsMet(this.config.visibility, this.hass);
+ this._setElementVisibility(visible);
+ }
+
+ private _setElementVisibility(visible: boolean) {
+ if (!this._element) return;
+
+ if (this.hidden !== !visible) {
+ this.style.setProperty("display", visible ? "" : "none");
+ this.toggleAttribute("hidden", !visible);
+ fireEvent(this, "heading-badge-visibility-changed", { value: visible });
+ }
+
+ if (!visible && this._element.parentElement) {
+ this.removeChild(this._element);
+ } else if (visible && !this._element.parentElement) {
+ this.appendChild(this._element);
+ }
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-heading-badge": HuiHeadingBadge;
+ }
+}
diff --git a/src/panels/lovelace/heading-badges/types.ts b/src/panels/lovelace/heading-badges/types.ts
new file mode 100644
index 0000000000..ac9a4a10cd
--- /dev/null
+++ b/src/panels/lovelace/heading-badges/types.ts
@@ -0,0 +1,25 @@
+import { ActionConfig } from "../../../data/lovelace/config/action";
+import { Condition } from "../common/validate-condition";
+
+export type LovelaceHeadingBadgeConfig = {
+ type?: string;
+ [key: string]: any;
+ visibility?: Condition[];
+};
+
+export interface ErrorBadgeConfig extends LovelaceHeadingBadgeConfig {
+ type: string;
+ error: string;
+ origConfig: LovelaceHeadingBadgeConfig;
+}
+
+export interface EntityHeadingBadgeConfig extends LovelaceHeadingBadgeConfig {
+ type?: "entity";
+ entity: string;
+ state_content?: string | string[];
+ icon?: string;
+ show_state?: boolean;
+ show_icon?: boolean;
+ color?: string;
+ tap_action?: ActionConfig;
+}
diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts
index ca9ae97654..7a30f3e1f8 100644
--- a/src/panels/lovelace/types.ts
+++ b/src/panels/lovelace/types.ts
@@ -13,6 +13,7 @@ import { LovelaceRow, LovelaceRowConfig } from "./entity-rows/types";
import { LovelaceHeaderFooterConfig } from "./header-footer/types";
import { LovelaceCardFeatureConfig } from "./card-features/types";
import { LovelaceElement, LovelaceElementConfig } from "./elements/types";
+import { LovelaceHeadingBadgeConfig } from "./heading-badges/types";
declare global {
// eslint-disable-next-line
@@ -178,3 +179,27 @@ export interface LovelaceCardFeatureEditor
extends LovelaceGenericElementEditor {
setConfig(config: LovelaceCardFeatureConfig): void;
}
+
+export interface LovelaceHeadingBadge extends HTMLElement {
+ hass?: HomeAssistant;
+ preview?: boolean;
+ setConfig(config: LovelaceHeadingBadgeConfig);
+}
+
+export interface LovelaceHeadingBadgeConstructor
+ extends Constructor {
+ getStubConfig?: (
+ hass: HomeAssistant,
+ stateObj?: HassEntity
+ ) => LovelaceHeadingBadgeConfig;
+ getConfigElement?: () => LovelaceHeadingBadgeEditor;
+ getConfigForm?: () => {
+ schema: HaFormSchema[];
+ assertConfig?: (config: LovelaceCardConfig) => void;
+ };
+}
+
+export interface LovelaceHeadingBadgeEditor
+ extends LovelaceGenericElementEditor {
+ setConfig(config: LovelaceHeadingBadgeConfig): void;
+}
diff --git a/src/translations/en.json b/src/translations/en.json
index ee8a3f1539..e68d165f77 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -6403,7 +6403,7 @@
"row": "Entity row editor",
"feature": "Feature editor",
"element": "Element editor",
- "heading-entity": "Entity editor",
+ "heading-badge": "Heading badge editor",
"element_type": "{type} element editor"
}
}