Entity state colors theming (#14831)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Paul Bottein 2023-01-23 09:28:38 +01:00 committed by GitHub
parent 1b922e0065
commit d15d339782
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 312 additions and 463 deletions

View File

@ -115,8 +115,8 @@ export class DemoHaBarSwitch extends LitElement {
font-weight: 600;
}
.custom {
--switch-bar-on-color: rgb(var(--rgb-green-color));
--switch-bar-off-color: rgb(var(--rgb-red-color));
--switch-bar-on-color: var(--green-color);
--switch-bar-off-color: var(--red-color);
--switch-bar-thickness: 100px;
--switch-bar-border-radius: 24px;
--switch-bar-padding: 6px;

View File

@ -104,16 +104,17 @@ const ENTITIES: HassEntity[] = [
createEntity("alarm_control_panel.disarming", "disarming"),
createEntity("alarm_control_panel.triggered", "triggered"),
// Alert
createEntity("alert.idle", "idle"),
createEntity("alert.off", "off"),
createEntity("alert.on", "on"),
createEntity("alert.idle", "idle"),
// Automation
createEntity("automation.off", "off"),
createEntity("automation.on", "on"),
// Binary Sensor
...BINARY_SENSOR_DEVICE_CLASSES.map((dc) =>
createEntity(`binary_sensor.${dc}`, "on", dc)
),
...BINARY_SENSOR_DEVICE_CLASSES.map((dc) => [
createEntity(`binary_sensor.${dc}`, "off", dc),
createEntity(`binary_sensor.${dc}`, "on", dc),
]).reduce((arr, item) => [...arr, ...item], []),
// Button
createEntity("button.restart", "unknown", "restart"),
createEntity("button.update", "unknown", "update"),
@ -142,6 +143,9 @@ const ENTITIES: HassEntity[] = [
createEntity("climate.auto_dry", "auto", undefined, {
hvac_action: "drying",
}),
createEntity("climate.auto_fan", "auto", undefined, {
hvac_action: "fan",
}),
// Cover
createEntity("cover.closing", "closing"),
createEntity("cover.closed", "closed"),
@ -180,8 +184,8 @@ const ENTITIES: HassEntity[] = [
createEntity("light.off", "off"),
createEntity("light.on", "on"),
// Locks
createEntity("lock.unlocked", "unlocked"),
createEntity("lock.locked", "locked"),
createEntity("lock.unlocked", "unlocked"),
createEntity("lock.locking", "locking"),
createEntity("lock.unlocking", "unlocking"),
createEntity("lock.jammed", "jammed"),
@ -205,17 +209,24 @@ const ENTITIES: HassEntity[] = [
createEntity("media_player.speaker_playing", "playing", "speaker"),
createEntity("media_player.speaker_paused", "paused", "speaker"),
createEntity("media_player.speaker_standby", "standby", "speaker"),
// Plant
createEntity("plant.ok", "ok"),
createEntity("plant.problem", "problem"),
// Remote
createEntity("remote.off", "off"),
createEntity("remote.on", "on"),
// Schedule
createEntity("schedule.off", "off"),
createEntity("schedule.on", "on"),
// Script
createEntity("script.off", "off"),
createEntity("script.on", "on"),
// Sensor
...SENSOR_DEVICE_CLASSES.map((dc) => createEntity(`sensor.${dc}`, "10", dc)),
// Battery sensor
...[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((value) =>
createEntity(`sensor.battery_${value}`, value.toString(), "battery")
...[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, "unknown", "not_valid"].map(
(value) =>
createEntity(`sensor.battery_${value}`, value.toString(), "battery")
),
// Siren
createEntity("siren.off", "off"),

View File

@ -19,7 +19,9 @@ export const THEME_COLORS = new Set([
"orange",
"deep-orange",
"brown",
"light-grey",
"grey",
"dark-grey",
"blue-grey",
"black",
"white",
@ -27,7 +29,7 @@ export const THEME_COLORS = new Set([
export function computeCssColor(color: string): string {
if (THEME_COLORS.has(color)) {
return `rgb(var(--rgb-${color}-color))`;
return `var(--${color}-color)`;
}
return color;
}

View File

@ -1,21 +0,0 @@
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 "arming":
case "disarming":
return "alarm-arming";
case "triggered":
return "alarm-triggered";
case "disarmed":
return "alarm-disarmed";
default:
return undefined;
}
};

View File

@ -1,10 +0,0 @@
export const alertColor = (state?: string): string | undefined => {
switch (state) {
case "on":
return "alert";
case "off":
return "alert-off";
default:
return undefined;
}
};

View File

@ -1,13 +1,15 @@
export const batteryStateColor = (state: string) => {
export const batteryStateColorProperty = (
state: string
): string | undefined => {
const value = Number(state);
if (isNaN(value)) {
return undefined;
}
if (value >= 70) {
return "sensor-battery-high";
return "--state-sensor-battery-high-color";
}
if (value >= 30) {
return "sensor-battery-medium";
return "--state-sensor-battery-medium-color";
}
return "sensor-battery-low";
return "--state-sensor-battery-low-color";
};

View File

@ -1,29 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { stateActive } from "../state_active";
const ALERTING_DEVICE_CLASSES = new Set([
"battery",
"carbon_monoxide",
"gas",
"heat",
"lock",
"moisture",
"problem",
"safety",
"smoke",
"tamper",
]);
export const binarySensorColor = (
stateObj: HassEntity,
state: string
): string | undefined => {
const deviceClass = stateObj?.attributes.device_class;
if (!stateActive(stateObj, state)) {
return undefined;
}
return deviceClass && ALERTING_DEVICE_CLASSES.has(deviceClass)
? "binary-sensor-alerting"
: "binary-sensor";
};

View File

@ -1,29 +0,0 @@
import { HvacAction } from "../../../data/climate";
export const CLIMATE_HVAC_ACTION_COLORS: Record<HvacAction, string> = {
cooling: "var(--rgb-state-climate-cool-color)",
drying: "var(--rgb-state-climate-dry-color)",
fan: "var(--rgb-state-climate-fan-only-color)",
heating: "var(--rgb-state-climate-heat-color)",
idle: "var(--rgb-state-climate-idle-color)",
off: "var(--rgb-state-climate-off-color)",
};
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

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

View File

@ -1,10 +0,0 @@
export const personColor = (state: string): string | undefined => {
switch (state) {
case "home":
return "person-home";
case "not_home":
return "person-not-home";
default:
return "person-zone";
}
};

View File

@ -1,15 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { batteryStateColor } from "./battery_color";
export const sensorColor = (
stateObj: HassEntity,
state: string
): string | undefined => {
const deviceClass = stateObj?.attributes.device_class;
if (deviceClass === "battery") {
return batteryStateColor(state);
}
return undefined;
};

View File

@ -1,15 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { UpdateEntity, updateIsInstalling } from "../../../data/update";
import { stateActive } from "../state_active";
export const updateColor = (
stateObj: HassEntity,
state: string
): string | undefined => {
if (!stateActive(stateObj, state)) {
return undefined;
}
return updateIsInstalling(stateObj as UpdateEntity)
? "update-installing"
: "update";
};

View File

@ -1,96 +1,102 @@
/** Return an color representing a state. */
import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE } from "../../data/entity";
import { alarmControlPanelColor } from "./color/alarm_control_panel_color";
import { alertColor } from "./color/alert_color";
import { binarySensorColor } from "./color/binary_sensor_color";
import { climateColor } from "./color/climate_color";
import { lockColor } from "./color/lock_color";
import { personColor } from "./color/person_color";
import { sensorColor } from "./color/sensor_color";
import { updateColor } from "./color/update_color";
import { computeCssVariable } from "../../resources/css-variables";
import { slugify } from "../string/slugify";
import { batteryStateColorProperty } from "./color/battery_color";
import { computeDomain } from "./compute_domain";
import { stateActive } from "./state_active";
const STATIC_ACTIVE_COLORED_DOMAIN = new Set([
const STATE_COLORED_DOMAIN = new Set([
"alarm_control_panel",
"alert",
"automation",
"binary_sensor",
"calendar",
"camera",
"climate",
"cover",
"device_tracker",
"fan",
"group",
"humidifier",
"input_boolean",
"light",
"lock",
"media_player",
"person",
"plant",
"remote",
"schedule",
"script",
"siren",
"sun",
"switch",
"timer",
"update",
"vacuum",
]);
export const stateColorCss = (stateObj: HassEntity, state?: string) => {
const compareState = state !== undefined ? state : stateObj?.state;
if (compareState === UNAVAILABLE) {
return `var(--rgb-state-unavailable-color)`;
return `var(--state-unavailable-color)`;
}
const domainColor = stateColor(stateObj, state);
if (domainColor) {
return `var(--rgb-state-${domainColor}-color)`;
}
if (!stateActive(stateObj, state)) {
return `var(--rgb-state-inactive-color)`;
const properties = stateColorProperties(stateObj, state);
if (properties) {
return computeCssVariable(properties);
}
return undefined;
};
export const stateColor = (stateObj: HassEntity, state?: string) => {
export const domainStateColorProperties = (
stateObj: HassEntity,
state?: string
): string[] => {
const compareState = state !== undefined ? state : stateObj.state;
const domain = computeDomain(stateObj.entity_id);
const active = stateActive(stateObj, state);
const properties: string[] = [];
const stateKey = slugify(compareState, "_");
const activeKey = active ? "active" : "inactive";
const dc = stateObj.attributes.device_class;
if (dc) {
properties.push(`--state-${domain}-${dc}-${stateKey}-color`);
}
properties.push(
`--state-${domain}-${stateKey}-color`,
`--state-${domain}-${activeKey}-color`,
`--state-${activeKey}-color`
);
return properties;
};
export const stateColorProperties = (
stateObj: HassEntity,
state?: string
): string[] | undefined => {
const compareState = state !== undefined ? state : stateObj?.state;
const domain = computeDomain(stateObj.entity_id);
const dc = stateObj.attributes.device_class;
if (
STATIC_ACTIVE_COLORED_DOMAIN.has(domain) &&
stateActive(stateObj, state)
) {
return domain.replace("_", "-");
// Special rules for battery coloring
if (domain === "sensor" && dc === "battery") {
const property = batteryStateColorProperty(compareState);
if (property) {
return [property];
}
}
switch (domain) {
case "alarm_control_panel":
return alarmControlPanelColor(compareState);
case "alert":
return alertColor(compareState);
case "binary_sensor":
return binarySensorColor(stateObj, compareState);
case "climate":
return climateColor(compareState);
case "lock":
return lockColor(compareState);
case "person":
case "device_tracker":
return personColor(compareState);
case "sensor":
return sensorColor(stateObj, compareState);
case "sun":
return compareState === "above_horizon" ? "sun-day" : "sun-night";
case "update":
return updateColor(stateObj, compareState);
if (STATE_COLORED_DOMAIN.has(domain)) {
return domainStateColorProperties(stateObj, state);
}
return undefined;

View File

@ -22,6 +22,6 @@ export const iconColorCSS = css`
/* Color the icon if unavailable */
ha-state-icon[data-state="unavailable"] {
color: rgb(var(--rgb-state-unavailable-color));
color: var(--state-unavailable-color);
}
`;

View File

@ -1,11 +1,11 @@
import { HassEntity } from "home-assistant-js-websocket";
import { getGraphColorByIndex } from "../../../common/color/colors";
import { lab2hex, rgb2hex, rgb2lab } from "../../../common/color/convert-color";
import { hex2rgb, lab2hex, rgb2lab } from "../../../common/color/convert-color";
import { labBrighten } from "../../../common/color/lab";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { stateColor } from "../../../common/entity/state_color";
import { UNAVAILABLE } from "../../../data/entity";
import { stateColorProperties } from "../../../common/entity/state_color";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity";
import { computeCssValue } from "../../../resources/css-variables";
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
media_player: {
@ -17,61 +17,35 @@ const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
},
};
const cssColorMap: Map<string, [number, number, number]> = new Map();
function cssToRgb(
cssVariable: string,
computedStyles: CSSStyleDeclaration
): [number, number, number] | undefined {
if (!cssVariable.startsWith("--rgb")) {
return undefined;
}
if (cssColorMap.has(cssVariable)) {
return cssColorMap.get(cssVariable)!;
}
const value = computedStyles.getPropertyValue(cssVariable);
if (!value) return undefined;
const rgb = value.split(",").map((v) => Number(v)) as [
number,
number,
number
];
cssColorMap.set(cssVariable, rgb);
return rgb;
}
function computeTimelineStateColor(
state: string,
computedStyles: CSSStyleDeclaration,
stateObj?: HassEntity
): string | undefined {
if (!stateObj || state === UNAVAILABLE) {
return "transparent";
return computeCssValue("--history-unavailable-color", computedStyles);
}
const color = stateColor(stateObj, state);
if (!color && !stateActive(stateObj, state)) {
const rgb = cssToRgb("--rgb-state-inactive-color", computedStyles);
if (!rgb) return undefined;
return rgb2hex(rgb);
if (state === UNKNOWN) {
return computeCssValue("--history-unknown-color", computedStyles);
}
const rgb = cssToRgb(`--rgb-state-${color}-color`, computedStyles);
const properties = stateColorProperties(stateObj, state);
if (!properties) {
return undefined;
}
const rgb = computeCssValue(properties, computedStyles);
if (!rgb) return undefined;
const domain = computeDomain(stateObj.entity_id);
const shade = DOMAIN_STATE_SHADES[domain]?.[state] as number | number;
if (!shade) {
return rgb2hex(rgb);
return rgb;
}
return lab2hex(labBrighten(rgb2lab(rgb), shade));
return lab2hex(labBrighten(rgb2lab(hex2rgb(rgb)), shade));
}
let colorIndex = 0;

View File

@ -11,13 +11,13 @@ import {
import { property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { CLIMATE_HVAC_ACTION_COLORS } from "../../common/entity/color/climate_color";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { stateActive } from "../../common/entity/state_active";
import { stateColorCss } from "../../common/entity/state_color";
import { iconColorCSS } from "../../common/style/icon_color_css";
import { cameraUrlWithWidthHeight } from "../../data/camera";
import { HVAC_ACTION_TO_MODE } from "../../data/climate";
import type { HomeAssistant } from "../../types";
import "../ha-state-icon";
@ -115,7 +115,7 @@ export class StateBadge extends LitElement {
} else if (this._stateColor && stateActive(stateObj)) {
const color = stateColorCss(stateObj);
if (color) {
iconStyle.color = `rgb(${color})`;
iconStyle.color = color;
}
if (stateObj.attributes.rgb_color) {
iconStyle.color = `rgb(${stateObj.attributes.rgb_color.join(",")})`;
@ -134,8 +134,11 @@ export class StateBadge extends LitElement {
}
if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action;
if (["heating", "cooling", "drying"].includes(hvacAction)) {
iconStyle.color = `rgb(${CLIMATE_HVAC_ACTION_COLORS[hvacAction]})`;
if (["heating", "cooling", "drying", "fan"].includes(hvacAction)) {
iconStyle.color = stateColorCss(
stateObj,
HVAC_ACTION_TO_MODE[hvacAction]
)!;
} else {
delete iconStyle.color;
}

View File

@ -271,8 +271,8 @@ export class HaBarSlider extends LitElement {
return css`
:host {
display: block;
--slider-bar-color: rgb(var(--rgb-primary-color));
--slider-bar-background: rgb(var(--rgb-disabled-color));
--slider-bar-color: var(--primary-color);
--slider-bar-background: var(--disabled-color);
--slider-bar-background-opacity: 0.2;
--slider-bar-thickness: 40px;
--slider-bar-border-radius: 10px;
@ -401,7 +401,7 @@ export class HaBarSlider extends LitElement {
.slider .slider-track-cursor:after {
display: block;
content: "";
background-color: rgb(var(--rgb-secondary-text-color));
background-color: var(--secondary-text-color);
position: absolute;
top: 0;
left: 0;

View File

@ -92,8 +92,8 @@ export class HaBarSwitch extends LitElement {
return css`
:host {
display: block;
--switch-bar-on-color: rgb(var(--rgb-primary-color));
--switch-bar-off-color: rgb(var(--rgb-disabled-color));
--switch-bar-on-color: var(--primary-color);
--switch-bar-off-color: var(--disabled-color);
--switch-bar-background-opacity: 0.2;
--switch-bar-thickness: 40px;
--switch-bar-border-radius: 12px;

View File

@ -21,8 +21,8 @@ export class HaTileBadge extends LitElement {
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));
--tile-badge-background-color: var(--primary-color);
--tile-badge-icon-color: var(--white-color);
--mdc-icon-size: 12px;
}
.badge {

View File

@ -82,9 +82,8 @@ export class HaTileButton extends LitElement {
return css`
:host {
--tile-button-icon-color: var(--primary-text-color);
--tile-button-background-color: rgb(var(--rgb-disabled-color));
--tile-button-background-color: var(--disabled-color);
--tile-button-background-opacity: 0.2;
--mdc-ripple-color: var(--tile-button-background-color);
width: 40px;
height: 40px;
-webkit-tap-highlight-color: transparent;
@ -107,6 +106,7 @@ export class HaTileButton extends LitElement {
outline: none;
overflow: hidden;
background: none;
--mdc-ripple-color: var(--tile-button-background-color);
}
.button::before {
content: "";
@ -128,7 +128,7 @@ export class HaTileButton extends LitElement {
}
.button:disabled {
cursor: not-allowed;
--tile-button-background-color: rgb(var(--rgb-disabled-color));
--tile-button-background-color: var(--disabled-color);
--tile-button-icon-color: var(--disabled-text-color);
--tile-button-background-opacity: 0.2;
}

View File

@ -22,7 +22,7 @@ export class HaTileIcon extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
--tile-icon-color: rgb(var(--rgb-disabled-color));
--tile-icon-color: var(--disabled-color);
--mdc-icon-size: 24px;
}
.shape::before {

View File

@ -47,13 +47,10 @@ export class HaTileSlider extends LitElement {
static get styles(): CSSResultGroup {
return css`
ha-bar-slider {
--slider-bar-color: var(
--tile-slider-color,
rgb(var(--rgb-primary-color))
);
--slider-bar-color: var(--tile-slider-color, var(--primary-color));
--slider-bar-background: var(
--tile-slider-background,
rgb(var(--rgb-disabled-color))
var(--disabled-color)
);
--slider-bar-background-opacity: var(
--tile-slider-background-opacity,

View File

@ -72,3 +72,12 @@ const hvacModeOrdering: { [key in HvacMode]: number } = {
export const compareClimateHvacModes = (mode1: HvacMode, mode2: HvacMode) =>
hvacModeOrdering[mode1] - hvacModeOrdering[mode2];
export const HVAC_ACTION_TO_MODE: Record<HvacAction, HvacMode> = {
cooling: "cool",
drying: "dry",
fan: "fan_only",
heating: "heat",
idle: "off",
off: "off",
};

View File

@ -577,7 +577,7 @@ class HaLogbookRenderer extends LitElement {
}
.indicator {
background-color: rgb(var(--rgb-disabled-color));
background-color: var(--disabled-color);
height: 8px;
width: 8px;
border-radius: 4px;

View File

@ -8,13 +8,15 @@ import {
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { alarmPanelIcon } from "../../../common/entity/alarm_panel_icon";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/ha-chip";
import type { HaTextField } from "../../../components/ha-textfield";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import {
callAlarmAction,
FORMAT_NUMBER,
@ -155,6 +157,9 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
stateLabel}
<ha-chip
hasIcon
style=${styleMap({
"--alarm-state-color": stateColorCss(stateObj),
})}
class=${classMap({ [stateObj.state]: true })}
@click=${this._handleMoreInfo}
>
@ -269,7 +274,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
flex-direction: column;
align-items: center;
box-sizing: border-box;
--alarm-state-color: rgb(var(--rgb-state-alarm-armed-color));
--alarm-state-color: var(--state-inactive-color);
}
ha-chip {
@ -286,26 +291,9 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
box-sizing: border-box;
}
.unavailable {
--alarm-state-color: rgb(var(--rgb-state-unavailable-color));
}
.disarmed {
--alarm-state-color: rgb(var(--rgb-state-alarm-disarmed-color));
}
.triggered {
--alarm-state-color: rgb(var(--rgb-state-alarm-triggered-color));
animation: pulse 1s infinite;
}
.arming {
--alarm-state-color: rgb(var(--rgb-state-alarm-arming-color));
animation: pulse 1s infinite;
}
.triggered,
.arming,
.pending {
--alarm-state-color: rgb(var(--rgb-state-alarm-pending-color));
animation: pulse 1s infinite;
}

View File

@ -563,7 +563,7 @@ export class HuiAreaCard
--mdc-icon-button-size: 44px;
}
.on {
color: rgb(var(--rgb-state-light-color));
color: var(--state-light-color);
}
`;
}

View File

@ -21,7 +21,6 @@ import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { DOMAINS_TOGGLE } from "../../../common/const";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { CLIMATE_HVAC_ACTION_COLORS } from "../../../common/entity/color/climate_color";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
@ -31,6 +30,7 @@ import { stateColorCss } from "../../../common/entity/state_color";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { iconColorCSS } from "../../../common/style/icon_color_css";
import "../../../components/ha-card";
import { HVAC_ACTION_TO_MODE } from "../../../data/climate";
import { LightEntity } from "../../../data/light";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
@ -324,14 +324,14 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
}
if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action;
if (["heating", "cooling", "drying"].includes(hvacAction)) {
return `rgb(${CLIMATE_HVAC_ACTION_COLORS[hvacAction]})`;
if (["heating", "cooling", "drying", "fan"].includes(hvacAction)) {
return stateColorCss(stateObj, HVAC_ACTION_TO_MODE[hvacAction]);
}
return undefined;
}
const iconColor = stateColorCss(stateObj);
if (iconColor) {
return `rgb(${iconColor})`;
return iconColor;
}
return undefined;
}

View File

@ -12,7 +12,6 @@ import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { CLIMATE_HVAC_ACTION_COLORS } from "../../../common/entity/color/climate_color";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
@ -27,6 +26,7 @@ import {
import { iconColorCSS } from "../../../common/style/icon_color_css";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import { HVAC_ACTION_TO_MODE } from "../../../data/climate";
import { isUnavailableState } from "../../../data/entity";
import { formatAttributeValue } from "../../../data/entity_attributes";
import { LightEntity } from "../../../data/light";
@ -197,8 +197,8 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
private _computeColor(stateObj: HassEntity): string | undefined {
if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action;
if (["heating", "cooling", "drying"].includes(hvacAction)) {
return `rgb(${CLIMATE_HVAC_ACTION_COLORS[hvacAction]})`;
if (["heating", "cooling", "drying", "fan"].includes(hvacAction)) {
return stateColorCss(stateObj, HVAC_ACTION_TO_MODE[hvacAction]);
}
return undefined;
}
@ -207,7 +207,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
}
const iconColor = stateColorCss(stateObj);
if (iconColor) {
return `rgb(${iconColor})`;
return iconColor;
}
return undefined;
}

View File

@ -330,7 +330,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
}
.light-button.state-on {
color: rgb(var(--rgb-state-light-color));
color: var(--state-light-color);
}
.light-button.state-unavailable {

View File

@ -21,12 +21,14 @@ import {
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { UNIT_F } from "../../../common/const";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateColorCss } from "../../../common/entity/state_color";
import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-card";
import type { HaCard } from "../../../components/ha-card";
@ -249,8 +251,8 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
return html`
<ha-card
class=${classMap({
[mode]: true,
style=${styleMap({
"--mode-color": stateColorCss(stateObj),
})}
>
<ha-icon-button
@ -470,37 +472,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
--name-font-size: 1.2rem;
--brightness-font-size: 1.2rem;
--rail-border-color: transparent;
}
.auto,
.heat_cool {
--mode-color: var(--state-climate-auto-color);
}
.cool {
--mode-color: var(--state-climate-cool-color);
}
.heat {
--mode-color: var(--state-climate-heat-color);
}
.manual {
--mode-color: var(--state-climate-manual-color);
}
.off {
--mode-color: var(--state-climate-off-color);
}
.fan_only {
--mode-color: var(--state-climate-fan_only-color);
}
.eco {
--mode-color: var(--state-climate-eco-color);
}
.dry {
--mode-color: var(--state-climate-dry-color);
}
.idle {
--mode-color: var(--state-climate-idle-color);
}
.unknown-mode {
--mode-color: var(--state-unknown-color);
--mode-color: var(--state-inactive-color);
}
.more-info {

View File

@ -11,9 +11,10 @@ import {
queryAsync,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import { hsv2rgb, rgb2hsv } from "../../../common/color/convert-color";
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
import { DOMAINS_TOGGLE } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
@ -148,7 +149,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
computeDomain(entity.entity_id) === "person" ||
computeDomain(entity.entity_id) === "device_tracker"
) {
return "rgb(var(--rgb-state-default-color))";
return undefined;
}
// Use light color if the light support rgb
@ -167,13 +168,11 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
hsvColor[1] = 0.4;
}
}
return `rgb(${hsv2rgb(hsvColor).join(",")})`;
return rgb2hex(hsv2rgb(hsvColor));
}
// Fallback to state color
const stateColor =
stateColorCss(entity) ?? "var(--rgb-state-default-color)";
return `rgb(${stateColor})`;
return stateColorCss(entity);
});
private _computeStateDisplay(stateObj: HassEntity): TemplateResult | string {
@ -271,7 +270,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
class="badge"
.iconPath=${mdiExclamationThick}
style=${styleMap({
"--tile-badge-background-color": `rgb(var(--rgb-red-color))`,
"--tile-badge-background-color": `var(--red-color)`,
})}
></ha-tile-badge>
</div>
@ -292,6 +291,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
const stateDisplay = this._computeStateDisplay(stateObj);
const active = stateActive(stateObj);
const color = this._computeStateColor(stateObj, this._config.color);
const style = {
@ -308,7 +308,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
);
return html`
<ha-card style=${styleMap(style)}>
<ha-card style=${styleMap(style)} class=${classMap({ active })}>
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : null}
<div class="tile">
<div
@ -339,7 +339,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
.icon=${badge.icon}
.iconPath=${badge.iconPath}
style=${styleMap({
"--tile-badge-background-color": `rgb(${badge.color})`,
"--tile-badge-background-color": badge.color,
})}
></ha-tile-badge>
`
@ -407,7 +407,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
static get styles(): CSSResultGroup {
return css`
:host {
--tile-color: rgb(var(--rgb-state-inactive-color));
--tile-color: var(--state-inactive-color);
-webkit-tap-highlight-color: transparent;
}
ha-card:has(ha-tile-info:focus-visible) {
@ -421,8 +421,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
// For safari overflow hidden
z-index: 0;
}
ha-card.disabled {
--tile-color: rgb(var(--rgb-disabled-color));
ha-card.active {
--tile-color: var(--state-icon-color);
}
[role="button"] {
cursor: pointer;

View File

@ -6,8 +6,12 @@ import {
mdiSnowflake,
mdiWaterPercent,
} from "@mdi/js";
import { CLIMATE_HVAC_ACTION_COLORS } from "../../../../../common/entity/color/climate_color";
import { ClimateEntity, HvacAction } from "../../../../../data/climate";
import { stateColorCss } from "../../../../../common/entity/state_color";
import {
ClimateEntity,
HvacAction,
HvacMode,
} from "../../../../../data/climate";
import { ComputeBadgeFunction } from "./tile-badge";
export const CLIMATE_HVAC_ACTION_ICONS: Record<HvacAction, string> = {
@ -19,6 +23,15 @@ export const CLIMATE_HVAC_ACTION_ICONS: Record<HvacAction, string> = {
off: mdiPower,
};
export const CLIMATE_HVAC_ACTION_MODE: Record<HvacAction, HvacMode> = {
cooling: "cool",
drying: "dry",
fan: "fan_only",
heating: "heat",
idle: "off",
off: "off",
};
export const computeClimateBadge: ComputeBadgeFunction = (stateObj) => {
const hvacAction = (stateObj as ClimateEntity).attributes.hvac_action;
@ -28,6 +41,6 @@ export const computeClimateBadge: ComputeBadgeFunction = (stateObj) => {
return {
iconPath: CLIMATE_HVAC_ACTION_ICONS[hvacAction],
color: CLIMATE_HVAC_ACTION_COLORS[hvacAction],
color: stateColorCss(stateObj, CLIMATE_HVAC_ACTION_MODE[hvacAction]),
};
};

View File

@ -1,5 +1,6 @@
import { mdiHome, mdiHomeExportOutline } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { stateColorCss } from "../../../../../common/entity/state_color";
import { HomeAssistant } from "../../../../../types";
import { ComputeBadgeFunction } from "./tile-badge";
@ -19,23 +20,12 @@ function personBadgeIcon(entity: HassEntity) {
return state === "not_home" ? mdiHomeExportOutline : mdiHome;
}
function personBadgeColor(entity: HassEntity) {
switch (entity.state) {
case "home":
return "var(--rgb-state-person-home-color)";
case "not_home":
return "var(--rgb-state-person-not-home-color)";
default:
return "var(--rgb-state-person-zone-color)";
}
}
export const computePersonBadge: ComputeBadgeFunction = (stateObj, hass) => {
const zone = getZone(stateObj, hass);
return {
iconPath: personBadgeIcon(stateObj),
icon: zone?.attributes.icon,
color: personBadgeColor(stateObj),
color: stateColorCss(stateObj),
};
};

View File

@ -7,7 +7,7 @@ import { computeClimateBadge } from "./tile-badge-climate";
import { computePersonBadge } from "./tile-badge-person";
export type TileBadge = {
color: string;
color?: string;
icon?: string;
iconPath?: string;
};
@ -23,7 +23,7 @@ export const computeTileBadge: ComputeBadgeFunction = (stateObj, hass) => {
}
if (stateObj.state === UNAVAILABLE) {
return {
color: "var(--rgb-orange-color)",
color: "var(--orange-color)",
iconPath: mdiExclamationThick,
};
}

View File

@ -0,0 +1,31 @@
export function computeCssVariable(
props: string | string[]
): string | undefined {
if (Array.isArray(props)) {
return props
.reverse()
.reduce<string | undefined>(
(str, variable) => `var(${variable}${str ? `, ${str}` : ""})`,
undefined
);
}
return `var(${props})`;
}
export function computeCssValue(
prop: string | string[],
computedStyles: CSSStyleDeclaration
): string | undefined {
if (Array.isArray(prop)) {
for (const property of prop) {
const value = computeCssValue(property, computedStyles);
if (value) return value;
}
return undefined;
}
if (!prop.endsWith("-color")) {
return undefined;
}
return computedStyles.getPropertyValue(prop).trim() || undefined;
}

View File

@ -57,30 +57,11 @@ documentContainer.innerHTML = `<custom-style>
--label-badge-yellow: #f4b400;
--label-badge-grey: #9e9e9e;
/* states */
/* states icon */
--state-icon-color: #44739e;
/* an error state is anything that would be considered an error */
/* --state-icon-error-color: #db4437; derived from error-color */
--state-on-color: #66a61e;
--state-off-color: #ff0029;
--state-home-color: #66a61e;
--state-not_home-color: #ff0029;
/* --state-unavailable-color: #a0a0a0; derived from disabled-text-color */
--state-unknown-color: #606060;
--state-idle-color: #7990a3;
/* climate state colors */
--state-climate-auto-color: #008000;
--state-climate-eco-color: #00ff7f;
--state-climate-cool-color: #2b9af9;
--state-climate-heat-color: #ff8100;
--state-climate-manual-color: #44739e;
--state-climate-off-color: #8a8a8a;
--state-climate-fan_only-color: #8a8a8a;
--state-climate-dry-color: #efbd07;
--state-climate-idle-color: #8a8a8a;
/* energy */
--energy-grid-consumption-color: #488fc2;
--energy-grid-return-color: #8353d1;
@ -106,87 +87,99 @@ documentContainer.innerHTML = `<custom-style>
/* 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-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-default-color: var(--rgb-dark-primary-color, 68, 115, 158);
--rgb-state-inactive-color: var(--rgb-grey-color);
--rgb-state-unavailable-color: var(--rgb-disabled-color);
/* color */
--disabled-color: #bdbdbd;
--red-color: #f44336;
--pink-color: #e91e63;
--purple-color: #9c27b0;
--deep-purple-color: #673ab7;
--indigo-color: #3f51b5;
--blue-color: #2196f3;
--light-blue-color: #03a9f4;
--cyan-color: #00bcd4;
--teal-color: #009688;
--green-color: #4caf50;
--light-green-color: #8bc34a;
--lime-color: #cddc39;
--yellow-color: #ffeb3b;
--amber-color: #ffc107;
--orange-color: #ff9800;
--deep-orange-color: #ff5722;
--brown-color: #795548;
--light-grey-color: #bdbdbd;
--grey-color: #9e9e9e;
--dark-grey-color: #606060;
--blue-grey-color: #607d8b;
--black-color: #000000;
--white-color: #ffffff;
/* rgb state domain colors */
--rgb-state-alarm-armed-color: var(--rgb-green-color);
--rgb-state-alarm-arming-color: var(--rgb-orange-color);
--rgb-state-alarm-disarmed-color: var(--rgb-grey-color);
--rgb-state-alarm-pending-color: var(--rgb-orange-color);
--rgb-state-alarm-triggered-color: var(--rgb-red-color);
--rgb-state-alert-color: var(--rgb-red-color);
--rgb-state-alert-off-color: var(--rgb-orange-color);
--rgb-state-automation-color: var(--rgb-amber-color);
--rgb-state-binary-sensor-alerting-color: var(--rgb-red-color);
--rgb-state-binary-sensor-color: var(--rgb-amber-color);
--rgb-state-calendar-color: var(--rgb-amber-color);
--rgb-state-camera-color: var(--rgb-amber-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-cyan-color);
--rgb-state-climate-heat-color: var(--rgb-deep-orange-color);
--rgb-state-climate-heat-cool-color: var(--rgb-amber-color);
--rgb-state-climate-idle-color: var(--rgb-grey-color);
--rgb-state-cover-color: var(--rgb-purple-color);
--rgb-state-fan-color: var(--rgb-cyan-color);
--rgb-state-group-color: var(--rgb-amber-color);
--rgb-state-humidifier-color: var(--rgb-blue-color);
--rgb-state-input-boolean-color: var(--rgb-amber-color);
--rgb-state-light-color: var(--rgb-amber-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-light-blue-color);
--rgb-state-person-home-color: var(--rgb-green-color);
--rgb-state-person-not-home-color: var(--rgb-grey-color);
--rgb-state-person-zone-color: var(--rgb-blue-color);
--rgb-state-plant-color: var(--rgb-red-color);
--rgb-state-remote-color: var(--rgb-amber-color);
--rgb-state-schedule-color: var(--rgb-amber-color);
--rgb-state-script-color: var(--rgb-amber-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-siren-color: var(--rgb-red-color);
--rgb-state-sun-day-color: var(--rgb-amber-color);
--rgb-state-sun-night-color: var(--rgb-deep-purple-color);
--rgb-state-switch-color: var(--rgb-amber-color);
--rgb-state-timer-color: var(--rgb-amber-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);
/* state color */
--state-active-color: var(--amber-color);
--state-inactive-color: var(--grey-color);
--state-unavailable-color: var(--disabled-color);
/* state domain colors */
--state-alarm_control_panel-armed_away-color: var(--green-color);
--state-alarm_control_panel-armed_custom_bypass-color: var(--green-color);
--state-alarm_control_panel-armed_home-color: var(--green-color);
--state-alarm_control_panel-armed_night-color: var(--green-color);
--state-alarm_control_panel-armed_vacation-color: var(--green-color);
--state-alarm_control_panel-arming-color: var(--orange-color);
--state-alarm_control_panel-disarming-color: var(--orange-color);
--state-alarm_control_panel-pending-color: var(--orange-color);
--state-alarm_control_panel-triggered-color: var(--red-color);
--state-alert-off-color: var(--orange-color);
--state-alert-on-color: var(--red-color);
--state-binary_sensor-battery-on-color: var(--red-color);
--state-binary_sensor-carbon_monoxide-on-color: var(--red-color);
--state-binary_sensor-color: var(--amber-color);
--state-binary_sensor-gas-on-color: var(--red-color);
--state-binary_sensor-heat-on-color: var(--red-color);
--state-binary_sensor-lock-on-color: var(--red-color);
--state-binary_sensor-moisture-on-color: var(--red-color);
--state-binary_sensor-problem-on-color: var(--red-color);
--state-binary_sensor-safety-on-color: var(--red-color);
--state-binary_sensor-smoke-on-color: var(--red-color);
--state-binary_sensor-sound-on-color: var(--red-color);
--state-binary_sensor-tamper-on-color: var(--red-color);
--state-climate-auto-color: var(--green-color);
--state-climate-cool-color: var(--blue-color);
--state-climate-dry-color: var(--orange-color);
--state-climate-fan_only-color: var(--cyan-color);
--state-climate-heat-color: var(--deep-orange-color);
--state-climate-heat-cool-color: var(--amber-color);
--state-cover-active-color: var(--purple-color);
--state-device_tracker-active-color: var(--blue-color);
--state-device_tracker-home-color: var(--green-color);
--state-fan-active-color: var(--cyan-color);
--state-humidifier-active-color: var(--blue-color);
--state-light-active-color: var(--amber-color);
--state-lock-jammed-color: var(--red-color);
--state-lock-locked-color: var(--green-color);
--state-lock-pending-color: var(--orange-color);
--state-lock-unlocked-color: var(--red-color);
--state-media_player-active-color: var(--light-blue-color);
--state-person-active-color: var(--blue-color);
--state-person-home-color: var(--green-color);
--state-plant-active-color: var(--red-color);
--state-siren-active-color: var(--red-color);
--state-sun-above_horizon-color: var(--amber-color);
--state-sun-below_horizon-color: var(--indigo-color);
--state-switch-active-color: var(--amber-color);
--state-update-active-color: var(--orange-color);
--state-vacuum-active-color: var(--teal-color);
--state-sensor-battery-high-color: var(--green-color);
--state-sensor-battery-low-color: var(--red-color);
--state-sensor-battery-medium-color: var(--orange-color);
/* history colors */
--history-unavailable-color: transparent;
--history-unknown-color: var(--dark-grey-color);
/* input components */
--input-idle-line-color: rgba(0, 0, 0, 0.42);

View File

@ -48,7 +48,7 @@ export const darkStyles = {
"energy-grid-return-color": "#a280db",
"map-filter":
"invert(.9) hue-rotate(170deg) brightness(1.5) contrast(1.2) saturate(.3)",
"rgb-disabled-color": "70, 70, 70",
"disabled-color": "#464646",
};
export const derivedStyles = {

View File

@ -4344,7 +4344,9 @@
"orange": "Orange",
"deep-orange": "Deep orange",
"brown": "Brown",
"light-grey": "Light grey",
"grey": "Grey",
"dark-grey": "Dark grey",
"blue-grey": "Blue grey",
"black": "Black",
"white": "White"

View File