mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-20 07:46:37 +00:00
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:
parent
66a75c4714
commit
c8d16af1b5
@ -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(", ");
|
||||
|
18
src/common/entity/color/alarm_control_panel_color.ts
Normal file
18
src/common/entity/color/alarm_control_panel_color.ts
Normal 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;
|
||||
}
|
||||
};
|
15
src/common/entity/color/battery_color.ts
Normal file
15
src/common/entity/color/battery_color.ts
Normal 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";
|
||||
};
|
20
src/common/entity/color/binary_sensor_color.ts
Normal file
20
src/common/entity/color/binary_sensor_color.ts
Normal 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";
|
||||
};
|
18
src/common/entity/color/climate_color.ts
Normal file
18
src/common/entity/color/climate_color.ts
Normal 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;
|
||||
}
|
||||
};
|
10
src/common/entity/color/cover_color.ts
Normal file
10
src/common/entity/color/cover_color.ts
Normal 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";
|
||||
};
|
15
src/common/entity/color/lock_color.ts
Normal file
15
src/common/entity/color/lock_color.ts
Normal 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;
|
||||
}
|
||||
};
|
32
src/common/entity/color/sensor_color.ts
Normal file
32
src/common/entity/color/sensor_color.ts
Normal 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";
|
||||
};
|
36
src/common/entity/state_active.ts
Normal file
36
src/common/entity/state_active.ts
Normal 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;
|
||||
}
|
||||
}
|
76
src/common/entity/state_color.ts
Normal file
76
src/common/entity/state_color.ts
Normal 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;
|
||||
};
|
@ -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 {
|
||||
|
@ -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];
|
||||
|
@ -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`
|
||||
<ha-card style=${styleMap(iconStyle)}>
|
||||
<ha-card style=${styleMap(style)}>
|
||||
<div class="tile">
|
||||
<ha-tile-icon
|
||||
.icon=${icon}
|
||||
@ -148,8 +153,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
--main-color: var(--rgb-disabled-color);
|
||||
--tap-padding: 6px;
|
||||
--tile-color: var(--rgb-disabled-color);
|
||||
--tile-tap-padding: 6px;
|
||||
}
|
||||
ha-card {
|
||||
height: 100%;
|
||||
@ -158,19 +163,19 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
background: rgba(var(--rgb-disabled-color), 0.1);
|
||||
}
|
||||
.tile {
|
||||
padding: calc(12px - var(--tap-padding));
|
||||
padding: calc(12px - var(--tile-tap-padding));
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
ha-tile-icon {
|
||||
padding: var(--tap-padding);
|
||||
padding: var(--tile-tap-padding);
|
||||
flex: none;
|
||||
margin-right: calc(12px - 2 * var(--tap-padding));
|
||||
margin-inline-end: calc(12px - 2 * var(--tap-padding));
|
||||
margin-right: calc(12px - 2 * var(--tile-tap-padding));
|
||||
margin-inline-end: calc(12px - 2 * var(--tile-tap-padding));
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
--color: var(--main-color);
|
||||
--color: var(--tile-color);
|
||||
transition: transform 180ms ease-in-out;
|
||||
}
|
||||
[role="button"] {
|
||||
@ -186,7 +191,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
ha-tile-info {
|
||||
padding: var(--tap-padding);
|
||||
padding: var(--tile-tap-padding);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 40px;
|
||||
@ -197,7 +202,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
outline: none;
|
||||
}
|
||||
ha-tile-info:focus-visible {
|
||||
background-color: rgba(var(--main-color), 0.1);
|
||||
background-color: rgba(var(--tile-color), 0.1);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
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 { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { domainIcon } from "../../../../common/entity/domain_icon";
|
||||
@ -65,13 +65,10 @@ export class HuiTileCardEditor
|
||||
select: {
|
||||
options: [
|
||||
{
|
||||
label: "Default",
|
||||
label: "Default (based on state)",
|
||||
value: "default",
|
||||
},
|
||||
...[
|
||||
...Array.from(THEME_COLORS),
|
||||
...Array.from(COLORS.keys()),
|
||||
].map((color) => ({
|
||||
...Array.from(THEME_COLORS).map((color) => ({
|
||||
label: capitalizeFirstLetter(color),
|
||||
value: color,
|
||||
})),
|
||||
|
@ -107,11 +107,71 @@ 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-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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user