Add state color to tile card (#14128)

* remove unused imports

* fix white color

* Introduce state color

* add color to css

* add battery colors

* Improve sensor color

* add binary sensor color

* add person color

* improve on/off state color

* add climate color

* only apply color when entity is activet push -f
This commit is contained in:
Paul Bottein 2022-10-25 14:02:09 +02:00 committed by GitHub
parent 66a75c4714
commit c8d16af1b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 352 additions and 51 deletions

View File

@ -1,38 +1,36 @@
import { hex2rgb } from "./convert-color"; import { hex2rgb } from "./convert-color";
export const THEME_COLORS = new Set(["primary", "accent", "disabled"]); export const THEME_COLORS = new Set([
"primary",
export const COLORS = new Map([ "accent",
["red", "#f44336"], "disabled",
["pink", "#e91e63"], "red",
["purple", "#9b27b0"], "pink",
["deep-purple", "#683ab7"], "purple",
["indigo", "#3f51b5"], "deep-purple",
["blue", "#2194f3"], "indigo",
["light-blue", "#2196f3"], "blue",
["cyan", "#03a8f4"], "light-blue",
["teal", "#009688"], "cyan",
["green", "#4caf50"], "teal",
["light-green", "#8bc34a"], "green",
["lime", "#ccdc39"], "light-green",
["yellow", "#ffeb3b"], "lime",
["amber", "#ffc107"], "yellow",
["orange", "#ff9800"], "amber",
["deep-orange", "#ff5722"], "orange",
["brown", "#795548"], "deep-orange",
["grey", "#9e9e9e"], "brown",
["blue-grey", "#607d8b"], "grey",
["black", "#000000"], "blue-grey",
["white", "ffffff"], "black",
"white",
]); ]);
export function computeRgbColor(color: string): string { export function computeRgbColor(color: string): string {
if (THEME_COLORS.has(color)) { if (THEME_COLORS.has(color)) {
return `var(--rgb-${color}-color)`; return `var(--rgb-${color}-color)`;
} }
if (COLORS.has(color)) {
return hex2rgb(COLORS.get(color)!).join(", ");
}
if (color.startsWith("#")) { if (color.startsWith("#")) {
try { try {
return hex2rgb(color).join(", "); return hex2rgb(color).join(", ");

View File

@ -0,0 +1,18 @@
export const alarmControlPanelColor = (state?: string): string | undefined => {
switch (state) {
case "armed_away":
case "armed_vacation":
case "armed_home":
case "armed_night":
case "armed_custom_bypass":
return "alarm-armed";
case "pending":
return "alarm-pending";
case "triggered":
return "alarm-triggered";
case "disarmed":
return "alarm-disarmed";
default:
return undefined;
}
};

View File

@ -0,0 +1,15 @@
import { HassEntity } from "home-assistant-js-websocket";
export const batteryStateColor = (stateObj: HassEntity) => {
const value = Number(stateObj.state);
if (isNaN(value)) {
return "sensor-battery-unknown";
}
if (value >= 70) {
return "sensor-battery-high";
}
if (value >= 30) {
return "sensor-battery-medium";
}
return "sensor-battery-low";
};

View File

@ -0,0 +1,20 @@
import { HassEntity } from "home-assistant-js-websocket";
const NORMAL_DEVICE_CLASSES = new Set([
"battery_charging",
"connectivity",
"light",
"moving",
"plug",
"power",
"presence",
"running",
]);
export const binarySensorColor = (stateObj: HassEntity): string | undefined => {
const deviceClass = stateObj?.attributes.device_class;
return deviceClass && NORMAL_DEVICE_CLASSES.has(deviceClass)
? "binary-sensor"
: "binary-sensor-danger";
};

View File

@ -0,0 +1,18 @@
export const climateColor = (state: string): string | undefined => {
switch (state) {
case "auto":
return "climate-auto";
case "cool":
return "climate-cool";
case "dry":
return "climate-dry";
case "fan_only":
return "climate-fan-only";
case "heat":
return "climate-heat";
case "heat_cool":
return "climate-heat-cool";
default:
return undefined;
}
};

View File

@ -0,0 +1,10 @@
import { HassEntity } from "home-assistant-js-websocket";
const SECURE_DEVICE_CLASSES = new Set(["door", "gate", "garage", "window"]);
export const coverColor = (stateObj?: HassEntity): string | undefined => {
const isSecure =
stateObj?.attributes.device_class &&
SECURE_DEVICE_CLASSES.has(stateObj.attributes.device_class);
return isSecure ? "cover-secure" : "cover";
};

View File

@ -0,0 +1,15 @@
export const lockColor = (state?: string): string | undefined => {
switch (state) {
case "locked":
return "lock-locked";
case "unlocked":
return "lock-unlocked";
case "jammed":
return "lock-jammed";
case "locking":
case "unlocking":
return "lock-pending";
default:
return undefined;
}
};

View File

@ -0,0 +1,32 @@
import { HassEntity } from "home-assistant-js-websocket";
import { batteryStateColor } from "./battery_color";
export const sensorColor = (stateObj: HassEntity): string | undefined => {
const deviceClass = stateObj?.attributes.device_class;
if (deviceClass === "battery") {
return batteryStateColor(stateObj);
}
switch (deviceClass) {
case "apparent_power":
case "current":
case "energy":
case "gas":
case "power_factor":
case "power":
case "reactive_power":
case "voltage":
return "sensor-energy";
case "temperature":
return "sensor-temperature";
case "humidity":
return "sensor-humidity";
case "illuminance":
return "sensor-illuminance";
case "moisture":
return "sensor-moisture";
}
return "sensor";
};

View File

@ -0,0 +1,36 @@
import { HassEntity } from "home-assistant-js-websocket";
import { OFF_STATES } from "../../data/entity";
import { computeDomain } from "./compute_domain";
const NORMAL_UNKNOWN_DOMAIN = ["button", "input_button", "scene"];
const NORMAL_OFF_DOMAIN = ["script"];
export function stateActive(stateObj: HassEntity): boolean {
const domain = computeDomain(stateObj.entity_id);
const state = stateObj.state;
if (
OFF_STATES.includes(state) &&
!(NORMAL_UNKNOWN_DOMAIN.includes(domain) && state === "unknown") &&
!(NORMAL_OFF_DOMAIN.includes(domain) && state === "script")
) {
return false;
}
// Custom cases
switch (domain) {
case "cover":
return state === "open" || state === "opening";
case "device_tracker":
case "person":
return state !== "not_home";
case "media-player":
return state !== "idle";
case "vacuum":
return state === "on" || state === "cleaning";
case "plant":
return state === "problem";
default:
return true;
}
}

View File

@ -0,0 +1,76 @@
/** Return an color representing a state. */
import { HassEntity } from "home-assistant-js-websocket";
import { UpdateEntity, updateIsInstalling } from "../../data/update";
import { alarmControlPanelColor } from "./color/alarm_control_panel_color";
import { binarySensorColor } from "./color/binary_sensor_color";
import { climateColor } from "./color/climate_color";
import { coverColor } from "./color/cover_color";
import { lockColor } from "./color/lock_color";
import { sensorColor } from "./color/sensor_color";
import { computeDomain } from "./compute_domain";
import { stateActive } from "./state_active";
export const stateColorCss = (stateObj?: HassEntity) => {
if (!stateObj || !stateActive(stateObj)) {
return `var(--rgb-disabled-color)`;
}
const color = stateColor(stateObj);
if (color) {
return `var(--rgb-state-${color}-color)`;
}
return `var(--rgb-primary-color)`;
};
export const stateColor = (stateObj: HassEntity) => {
const state = stateObj.state;
const domain = computeDomain(stateObj.entity_id);
switch (domain) {
case "alarm_control_panel":
return alarmControlPanelColor(state);
case "binary_sensor":
return binarySensorColor(stateObj);
case "cover":
return coverColor(stateObj);
case "climate":
return climateColor(state);
case "lock":
return lockColor(state);
case "light":
return "light";
case "humidifier":
return "humidifier";
case "media_player":
return "media-player";
case "person":
case "device_tracker":
return "person";
case "sensor":
return sensorColor(stateObj);
case "vacuum":
return "vacuum";
case "sun":
return state === "above_horizon" ? "sun-day" : "sun-night";
case "update":
return updateIsInstalling(stateObj as UpdateEntity)
? "update-installing"
: "update";
}
return undefined;
};

View File

@ -1,7 +1,5 @@
import { CSSResultGroup, html, css, LitElement, TemplateResult } from "lit"; import { CSSResultGroup, html, css, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../ha-icon";
import "../ha-svg-icon";
@customElement("ha-tile-info") @customElement("ha-tile-info")
export class HaTileInfo extends LitElement { export class HaTileInfo extends LitElement {

View File

@ -1,4 +1,7 @@
export const UNAVAILABLE = "unavailable"; export const UNAVAILABLE = "unavailable";
export const UNKNOWN = "unknown"; export const UNKNOWN = "unknown";
export const ON = "on";
export const OFF = "off";
export const UNAVAILABLE_STATES = [UNAVAILABLE, UNKNOWN]; export const UNAVAILABLE_STATES = [UNAVAILABLE, UNKNOWN];
export const OFF_STATES = [UNAVAILABLE, UNKNOWN, OFF];

View File

@ -3,9 +3,11 @@ import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { computeRgbColor } from "../../../common/color/compute-color"; import { computeRgbColor } from "../../../common/color/compute-color";
import { DOMAINS_TOGGLE, STATES_OFF } from "../../../common/const"; import { DOMAINS_TOGGLE } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDisplay } from "../../../common/entity/compute_state_display"; import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateIconPath } from "../../../common/entity/state_icon_path"; import { stateIconPath } from "../../../common/entity/state_icon_path";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/tile/ha-tile-icon"; import "../../../components/tile/ha-tile-icon";
@ -116,13 +118,16 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
this.hass.locale this.hass.locale
); );
const iconStyle = {}; const style = {
if (this._config.color && !STATES_OFF.includes(entity.state)) { "--tile-color": this._config.color
iconStyle["--main-color"] = computeRgbColor(this._config.color); ? stateActive(entity)
} ? computeRgbColor(this._config.color)
: undefined
: stateColorCss(entity),
};
return html` return html`
<ha-card style=${styleMap(iconStyle)}> <ha-card style=${styleMap(style)}>
<div class="tile"> <div class="tile">
<ha-tile-icon <ha-tile-icon
.icon=${icon} .icon=${icon}
@ -148,8 +153,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {
--main-color: var(--rgb-disabled-color); --tile-color: var(--rgb-disabled-color);
--tap-padding: 6px; --tile-tap-padding: 6px;
} }
ha-card { ha-card {
height: 100%; height: 100%;
@ -158,19 +163,19 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
background: rgba(var(--rgb-disabled-color), 0.1); background: rgba(var(--rgb-disabled-color), 0.1);
} }
.tile { .tile {
padding: calc(12px - var(--tap-padding)); padding: calc(12px - var(--tile-tap-padding));
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
} }
ha-tile-icon { ha-tile-icon {
padding: var(--tap-padding); padding: var(--tile-tap-padding);
flex: none; flex: none;
margin-right: calc(12px - 2 * var(--tap-padding)); margin-right: calc(12px - 2 * var(--tile-tap-padding));
margin-inline-end: calc(12px - 2 * var(--tap-padding)); margin-inline-end: calc(12px - 2 * var(--tile-tap-padding));
margin-inline-start: initial; margin-inline-start: initial;
direction: var(--direction); direction: var(--direction);
--color: var(--main-color); --color: var(--tile-color);
transition: transform 180ms ease-in-out; transition: transform 180ms ease-in-out;
} }
[role="button"] { [role="button"] {
@ -186,7 +191,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
transform: scale(1.2); transform: scale(1.2);
} }
ha-tile-info { ha-tile-info {
padding: var(--tap-padding); padding: var(--tile-tap-padding);
flex: 1; flex: 1;
min-width: 0; min-width: 0;
min-height: 40px; min-height: 40px;
@ -197,7 +202,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
outline: none; outline: none;
} }
ha-tile-info:focus-visible { ha-tile-info:focus-visible {
background-color: rgba(var(--main-color), 0.1); background-color: rgba(var(--tile-color), 0.1);
} }
`; `;
} }

View File

@ -3,7 +3,7 @@ import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { assert, assign, object, optional, string } from "superstruct"; import { assert, assign, object, optional, string } from "superstruct";
import { COLORS, THEME_COLORS } from "../../../../common/color/compute-color"; import { THEME_COLORS } from "../../../../common/color/compute-color";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain"; import { computeDomain } from "../../../../common/entity/compute_domain";
import { domainIcon } from "../../../../common/entity/domain_icon"; import { domainIcon } from "../../../../common/entity/domain_icon";
@ -65,13 +65,10 @@ export class HuiTileCardEditor
select: { select: {
options: [ options: [
{ {
label: "Default", label: "Default (based on state)",
value: "default", value: "default",
}, },
...[ ...Array.from(THEME_COLORS).map((color) => ({
...Array.from(THEME_COLORS),
...Array.from(COLORS.keys()),
].map((color) => ({
label: capitalizeFirstLetter(color), label: capitalizeFirstLetter(color),
value: color, value: color,
})), })),

View File

@ -107,11 +107,71 @@ documentContainer.innerHTML = `<custom-style>
/* rgb */ /* rgb */
--rgb-primary-color: 3, 169, 244; --rgb-primary-color: 3, 169, 244;
--rgb-accent-color: 255, 152, 0; --rgb-accent-color: 255, 152, 0;
--rgb-disabled-color: 189, 189, 189;
--rgb-primary-text-color: 33, 33, 33; --rgb-primary-text-color: 33, 33, 33;
--rgb-secondary-text-color: 114, 114, 114; --rgb-secondary-text-color: 114, 114, 114;
--rgb-text-primary-color: 255, 255, 255; --rgb-text-primary-color: 255, 255, 255;
--rgb-card-background-color: 255, 255, 255; --rgb-card-background-color: 255, 255, 255;
--rgb-disabled-color: 189, 189, 189; --rgb-red-color: 244, 67, 54;
--rgb-pink-color: 233, 30, 99;
--rgb-purple-color: 156, 39, 176;
--rgb-deep-purple-color: 103, 58, 183;
--rgb-indigo-color: 63, 81, 181;
--rgb-blue-color: 33, 150, 243;
--rgb-light-blue-color: 3, 169, 244;
--rgb-cyan-color: 0, 188, 212;
--rgb-teal-color: 0, 150, 136;
--rgb-green-color: 76, 175, 80;
--rgb-light-green-color: 139, 195, 74;
--rgb-lime-color: 205, 220, 57;
--rgb-yellow-color: 255, 235, 59;
--rgb-amber-color: 255, 193, 7;
--rgb-orange-color: 255, 152, 0;
--rgb-deep-orange-color: 255, 87, 34;
--rgb-brown-color: 121, 85, 72;
--rgb-grey-color: 158, 158, 158;
--rgb-blue-grey-color: 96, 125, 139;
--rgb-black-color: 0, 0, 0;
--rgb-white-color: 255, 255, 255;
/* rgb state color */
--rgb-state-alarm-armed-color: var(--rgb-green-color);
--rgb-state-alarm-disarmed-color: var(--rgb-primary-color);
--rgb-state-alarm-pending-color: var(--rgb-orange-color);
--rgb-state-alarm-triggered-color: var(--rgb-red-color);
--rgb-state-binary-sensor-color: var(--rgb-green-color);
--rgb-state-binary-sensor-danger-color: var(--rgb-red-color);
--rgb-state-cover-color: var(--rgb-blue-color);
--rgb-state-cover-secure-color: var(--rgb-red-color);
--rgb-state-humidifier-color: var(--rgb-deep-purple-color);
--rgb-state-light-color: var(--rgb-orange-color);
--rgb-state-lock-jammed-color: var(--rgb-red-color);
--rgb-state-lock-locked-color: var(--rgb-green-color);
--rgb-state-lock-pending-color: var(--rgb-orange-color);
--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-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);
--rgb-state-sensor-battery-unknown-color: var(--rgb-disabled-color);
--rgb-state-sensor-color: var(--rgb-blue-grey-color);
--rgb-state-sensor-energy-color: var(--rgb-amber-color);
--rgb-state-sensor-humidity-color: var(--rgb-deep-purple-color);
--rgb-state-sensor-illuminance-color: var(--rgb-amber-color);
--rgb-state-sensor-moisture-color: var(--rgb-light-blue-color);
--rgb-state-sensor-temperature-color: var(--rgb-deep-orange-color);
--rgb-state-sun-day-color: var(--rgb-amber-color);
--rgb-state-sun-night-color: var(--rgb-deep-purple-color);
--rgb-state-update-color: var(--rgb-green-color);
--rgb-state-update-installing-color: var(--rgb-orange-color);
--rgb-state-vacuum-color: var(--rgb-teal-color);
--rgb-state-climate-auto-color: var(--rgb-green-color);
--rgb-state-climate-cool-color: var(--rgb-blue-color);
--rgb-state-climate-dry-color: var(--rgb-orange-color);
--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);
/* input components */ /* input components */
--input-idle-line-color: rgba(0, 0, 0, 0.42); --input-idle-line-color: rgba(0, 0, 0, 0.42);