From c8d16af1b52837bdaa281eb9e8d290ff28ea9398 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 25 Oct 2022 14:02:09 +0200 Subject: [PATCH] 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 --- src/common/color/compute-color.ts | 52 ++++++------- .../entity/color/alarm_control_panel_color.ts | 18 +++++ src/common/entity/color/battery_color.ts | 15 ++++ .../entity/color/binary_sensor_color.ts | 20 +++++ src/common/entity/color/climate_color.ts | 18 +++++ src/common/entity/color/cover_color.ts | 10 +++ src/common/entity/color/lock_color.ts | 15 ++++ src/common/entity/color/sensor_color.ts | 32 ++++++++ src/common/entity/state_active.ts | 36 +++++++++ src/common/entity/state_color.ts | 76 +++++++++++++++++++ src/components/tile/ha-tile-info.ts | 2 - src/data/entity.ts | 3 + src/panels/lovelace/cards/hui-tile-card.ts | 35 +++++---- .../config-elements/hui-tile-card-editor.ts | 9 +-- src/resources/ha-style.ts | 62 ++++++++++++++- 15 files changed, 352 insertions(+), 51 deletions(-) create mode 100644 src/common/entity/color/alarm_control_panel_color.ts create mode 100644 src/common/entity/color/battery_color.ts create mode 100644 src/common/entity/color/binary_sensor_color.ts create mode 100644 src/common/entity/color/climate_color.ts create mode 100644 src/common/entity/color/cover_color.ts create mode 100644 src/common/entity/color/lock_color.ts create mode 100644 src/common/entity/color/sensor_color.ts create mode 100644 src/common/entity/state_active.ts create mode 100644 src/common/entity/state_color.ts diff --git a/src/common/color/compute-color.ts b/src/common/color/compute-color.ts index 0a97b7aad4..e7e3409a9d 100644 --- a/src/common/color/compute-color.ts +++ b/src/common/color/compute-color.ts @@ -1,38 +1,36 @@ import { hex2rgb } from "./convert-color"; -export const THEME_COLORS = new Set(["primary", "accent", "disabled"]); - -export const COLORS = new Map([ - ["red", "#f44336"], - ["pink", "#e91e63"], - ["purple", "#9b27b0"], - ["deep-purple", "#683ab7"], - ["indigo", "#3f51b5"], - ["blue", "#2194f3"], - ["light-blue", "#2196f3"], - ["cyan", "#03a8f4"], - ["teal", "#009688"], - ["green", "#4caf50"], - ["light-green", "#8bc34a"], - ["lime", "#ccdc39"], - ["yellow", "#ffeb3b"], - ["amber", "#ffc107"], - ["orange", "#ff9800"], - ["deep-orange", "#ff5722"], - ["brown", "#795548"], - ["grey", "#9e9e9e"], - ["blue-grey", "#607d8b"], - ["black", "#000000"], - ["white", "ffffff"], +export const THEME_COLORS = new Set([ + "primary", + "accent", + "disabled", + "red", + "pink", + "purple", + "deep-purple", + "indigo", + "blue", + "light-blue", + "cyan", + "teal", + "green", + "light-green", + "lime", + "yellow", + "amber", + "orange", + "deep-orange", + "brown", + "grey", + "blue-grey", + "black", + "white", ]); export function computeRgbColor(color: string): string { if (THEME_COLORS.has(color)) { return `var(--rgb-${color}-color)`; } - if (COLORS.has(color)) { - return hex2rgb(COLORS.get(color)!).join(", "); - } if (color.startsWith("#")) { try { return hex2rgb(color).join(", "); diff --git a/src/common/entity/color/alarm_control_panel_color.ts b/src/common/entity/color/alarm_control_panel_color.ts new file mode 100644 index 0000000000..2b862af450 --- /dev/null +++ b/src/common/entity/color/alarm_control_panel_color.ts @@ -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; + } +}; diff --git a/src/common/entity/color/battery_color.ts b/src/common/entity/color/battery_color.ts new file mode 100644 index 0000000000..839dbe8c39 --- /dev/null +++ b/src/common/entity/color/battery_color.ts @@ -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"; +}; diff --git a/src/common/entity/color/binary_sensor_color.ts b/src/common/entity/color/binary_sensor_color.ts new file mode 100644 index 0000000000..b5f9b05e9c --- /dev/null +++ b/src/common/entity/color/binary_sensor_color.ts @@ -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"; +}; diff --git a/src/common/entity/color/climate_color.ts b/src/common/entity/color/climate_color.ts new file mode 100644 index 0000000000..5da4098a9b --- /dev/null +++ b/src/common/entity/color/climate_color.ts @@ -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; + } +}; diff --git a/src/common/entity/color/cover_color.ts b/src/common/entity/color/cover_color.ts new file mode 100644 index 0000000000..1ff2bc6dc8 --- /dev/null +++ b/src/common/entity/color/cover_color.ts @@ -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"; +}; diff --git a/src/common/entity/color/lock_color.ts b/src/common/entity/color/lock_color.ts new file mode 100644 index 0000000000..dc677d3662 --- /dev/null +++ b/src/common/entity/color/lock_color.ts @@ -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; + } +}; diff --git a/src/common/entity/color/sensor_color.ts b/src/common/entity/color/sensor_color.ts new file mode 100644 index 0000000000..ad79ac43b5 --- /dev/null +++ b/src/common/entity/color/sensor_color.ts @@ -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"; +}; diff --git a/src/common/entity/state_active.ts b/src/common/entity/state_active.ts new file mode 100644 index 0000000000..f67c395b52 --- /dev/null +++ b/src/common/entity/state_active.ts @@ -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; + } +} diff --git a/src/common/entity/state_color.ts b/src/common/entity/state_color.ts new file mode 100644 index 0000000000..cec598d598 --- /dev/null +++ b/src/common/entity/state_color.ts @@ -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; +}; diff --git a/src/components/tile/ha-tile-info.ts b/src/components/tile/ha-tile-info.ts index f211b5490d..e56ed6aa3f 100644 --- a/src/components/tile/ha-tile-info.ts +++ b/src/components/tile/ha-tile-info.ts @@ -1,7 +1,5 @@ import { CSSResultGroup, html, css, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; -import "../ha-icon"; -import "../ha-svg-icon"; @customElement("ha-tile-info") export class HaTileInfo extends LitElement { diff --git a/src/data/entity.ts b/src/data/entity.ts index 829ee58c35..dbb7f759cc 100644 --- a/src/data/entity.ts +++ b/src/data/entity.ts @@ -1,4 +1,7 @@ export const UNAVAILABLE = "unavailable"; export const UNKNOWN = "unknown"; +export const ON = "on"; +export const OFF = "off"; export const UNAVAILABLE_STATES = [UNAVAILABLE, UNKNOWN]; +export const OFF_STATES = [UNAVAILABLE, UNKNOWN, OFF]; diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts index e9893555b7..8cf23befbb 100644 --- a/src/panels/lovelace/cards/hui-tile-card.ts +++ b/src/panels/lovelace/cards/hui-tile-card.ts @@ -3,9 +3,11 @@ import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; 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 { 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 "../../../components/ha-card"; import "../../../components/tile/ha-tile-icon"; @@ -116,13 +118,16 @@ export class HuiTileCard extends LitElement implements LovelaceCard { this.hass.locale ); - const iconStyle = {}; - if (this._config.color && !STATES_OFF.includes(entity.state)) { - iconStyle["--main-color"] = computeRgbColor(this._config.color); - } + const style = { + "--tile-color": this._config.color + ? stateActive(entity) + ? computeRgbColor(this._config.color) + : undefined + : stateColorCss(entity), + }; return html` - +
({ + ...Array.from(THEME_COLORS).map((color) => ({ label: capitalizeFirstLetter(color), value: color, })), diff --git a/src/resources/ha-style.ts b/src/resources/ha-style.ts index 5a903b26bd..fbfcf3f040 100644 --- a/src/resources/ha-style.ts +++ b/src/resources/ha-style.ts @@ -107,11 +107,71 @@ documentContainer.innerHTML = ` /* rgb */ --rgb-primary-color: 3, 169, 244; --rgb-accent-color: 255, 152, 0; + --rgb-disabled-color: 189, 189, 189; --rgb-primary-text-color: 33, 33, 33; --rgb-secondary-text-color: 114, 114, 114; --rgb-text-primary-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-idle-line-color: rgba(0, 0, 0, 0.42);