diff --git a/src/components/tile/ha-tile-badge.ts b/src/components/tile/ha-tile-badge.ts
new file mode 100644
index 0000000000..183f217109
--- /dev/null
+++ b/src/components/tile/ha-tile-badge.ts
@@ -0,0 +1,51 @@
+import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
+import { customElement, property } from "lit/decorators";
+import "../ha-icon";
+
+@customElement("ha-tile-badge")
+export class HaTileBadge extends LitElement {
+ @property() public iconPath?: string;
+
+ @property() public icon?: string;
+
+ protected render(): TemplateResult {
+ return html`
+
+ ${this.icon
+ ? html``
+ : html``}
+
+ `;
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ :host {
+ --tile-badge-background-color: rgb(var(--rgb-primary-color));
+ --tile-badge-icon-color: rgb(var(--rgb-white-color));
+ --mdc-icon-size: 12px;
+ }
+ .badge {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ line-height: 0;
+ width: 16px;
+ height: 16px;
+ border-radius: 8px;
+ background-color: var(--tile-badge-background-color);
+ transition: background-color 280ms ease-in-out;
+ }
+ .badge ha-icon,
+ .badge ha-svg-icon {
+ color: var(--tile-badge-icon-color);
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-tile-badge": HaTileBadge;
+ }
+}
diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts
index 249b6db5fe..c133b86865 100644
--- a/src/panels/lovelace/cards/hui-tile-card.ts
+++ b/src/panels/lovelace/cards/hui-tile-card.ts
@@ -11,6 +11,7 @@ import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateIconPath } from "../../../common/entity/state_icon_path";
import "../../../components/ha-card";
+import "../../../components/tile/ha-tile-badge";
import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-image";
import "../../../components/tile/ha-tile-info";
@@ -21,6 +22,7 @@ import { actionHandler } from "../common/directives/action-handler-directive";
import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import { LovelaceCard, LovelaceCardEditor } from "../types";
+import { computeTileBadge } from "./tile/badges/tile-badge";
import { ThermostatCardConfig, TileCardConfig } from "./types";
@customElement("hui-tile-card")
@@ -116,7 +118,9 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
return html`
-
+
+
+
- ${imageUrl
- ? html`
-
- `
- : html`
-
- `}
+
+ ${imageUrl
+ ? html`
+
+ `
+ : html`
+
+ `}
+ ${badge
+ ? html`
+
+ `
+ : null}
+
= {
+ cooling: "var(--rgb-state-climate-cool-color)",
+ drying: "var(--rgb-state-climate-dry-color)",
+ heating: "var(--rgb-state-climate-heat-color)",
+ idle: "var(--rgb-state-climate-idle-color)",
+ off: "var(--rgb-state-climate-off-color)",
+};
+
+export const CLIMATE_HVAC_ACTION_ICONS: Record = {
+ cooling: mdiSnowflake,
+ drying: mdiWaterPercent,
+ heating: mdiFire,
+ idle: mdiClockOutline,
+ off: mdiPower,
+};
+
+export const computeClimateBadge: ComputeBadgeFunction = (stateObj) => {
+ const hvacAction = (stateObj as ClimateEntity).attributes.hvac_action;
+
+ if (!hvacAction || hvacAction === "off") {
+ return undefined;
+ }
+
+ return {
+ iconPath: CLIMATE_HVAC_ACTION_ICONS[hvacAction],
+ color: CLIMATE_HVAC_ACTION_COLORS[hvacAction],
+ };
+};
diff --git a/src/panels/lovelace/cards/tile/badges/tile-badge-person.ts b/src/panels/lovelace/cards/tile/badges/tile-badge-person.ts
new file mode 100644
index 0000000000..7eaa3ea340
--- /dev/null
+++ b/src/panels/lovelace/cards/tile/badges/tile-badge-person.ts
@@ -0,0 +1,44 @@
+import { mdiHelp, mdiHome, mdiHomeExportOutline } from "@mdi/js";
+import { HassEntity } from "home-assistant-js-websocket";
+import { UNAVAILABLE_STATES } from "../../../../../data/entity";
+import { HomeAssistant } from "../../../../../types";
+import { ComputeBadgeFunction } from "./tile-badge";
+
+function getZone(entity: HassEntity, hass: HomeAssistant) {
+ const state = entity.state;
+ if (state === "home" || state === "not_home") return undefined;
+
+ const zones = Object.values(hass.states).filter((stateObj) =>
+ stateObj.entity_id.startsWith("zone.")
+ );
+
+ return zones.find((z) => state === z.attributes.friendly_name);
+}
+
+function personBadgeIcon(entity: HassEntity) {
+ const state = entity.state;
+ if (UNAVAILABLE_STATES.includes(state)) {
+ return mdiHelp;
+ }
+ return state === "not_home" ? mdiHomeExportOutline : mdiHome;
+}
+
+function personBadgeColor(entity: HassEntity, inZone?: boolean) {
+ if (inZone) {
+ return "var(--rgb-state-person-zone-color)";
+ }
+ const state = entity.state;
+ return state === "not_home"
+ ? "var(--rgb-state-person-not-home-color)"
+ : "var(--rgb-state-person-home-color)";
+}
+
+export const computePersonBadge: ComputeBadgeFunction = (stateObj, hass) => {
+ const zone = getZone(stateObj, hass);
+
+ return {
+ iconPath: personBadgeIcon(stateObj),
+ icon: zone?.attributes.icon,
+ color: personBadgeColor(stateObj, Boolean(zone)),
+ };
+};
diff --git a/src/panels/lovelace/cards/tile/badges/tile-badge.ts b/src/panels/lovelace/cards/tile/badges/tile-badge.ts
new file mode 100644
index 0000000000..eebda6b5e4
--- /dev/null
+++ b/src/panels/lovelace/cards/tile/badges/tile-badge.ts
@@ -0,0 +1,29 @@
+import { HassEntity } from "home-assistant-js-websocket";
+import { computeDomain } from "../../../../../common/entity/compute_domain";
+import { HomeAssistant } from "../../../../../types";
+import { computeClimateBadge } from "./tile-badge-climate";
+import { computePersonBadge } from "./tile-badge-person";
+
+export type TileBadge = {
+ color: string;
+ icon?: string;
+ iconPath?: string;
+};
+
+export type ComputeBadgeFunction = (
+ stateObj: HassEntity,
+ hass: HomeAssistant
+) => TileBadge | undefined;
+
+export const computeTileBadge: ComputeBadgeFunction = (stateObj, hass) => {
+ const domain = computeDomain(stateObj.entity_id);
+ switch (domain) {
+ case "person":
+ case "device_tracker":
+ return computePersonBadge(stateObj, hass);
+ case "climate":
+ return computeClimateBadge(stateObj, hass);
+ default:
+ return undefined;
+ }
+};
diff --git a/src/resources/ha-style.ts b/src/resources/ha-style.ts
index 96c99e47b3..49e3e8a8f6 100644
--- a/src/resources/ha-style.ts
+++ b/src/resources/ha-style.ts
@@ -152,6 +152,9 @@ documentContainer.innerHTML = `
--rgb-state-lock-unlocked-color: var(--rgb-red-color);
--rgb-state-media-player-color: var(--rgb-indigo-color);
--rgb-state-person-color: var(--rgb-blue-grey-color);
+ --rgb-state-person-home-color: var(--rgb-green-color);
+ --rgb-state-person-not-home-color: var(--rgb-red-color);
+ --rgb-state-person-zone-color: var(--rgb-blue-color);
--rgb-state-sensor-battery-high-color: var(--rgb-green-color);
--rgb-state-sensor-battery-low-color: var(--rgb-red-color);
--rgb-state-sensor-battery-medium-color: var(--rgb-orange-color);
@@ -173,6 +176,7 @@ documentContainer.innerHTML = `
--rgb-state-climate-fan-only-color: var(--rgb-teal-color);
--rgb-state-climate-heat-color: var(--rgb-deep-orange-color);
--rgb-state-climate-heat-cool-color: var(--rgb-green-color);
+ --rgb-state-climate-idle-color: var(--rgb-disabled-color);
/* input components */
--input-idle-line-color: rgba(0, 0, 0, 0.42);