Compare commits

..
Author SHA1 Message Date
Petar Petrov a2fe8a2f6e Allow theming timeline colors of custom states
States of domains without state colors now use --state-<domain>-<state>-color (or its device class variant) when the theme defines it, before falling back to the graph palette.
2026-11-15 16:41:23 +02:00
3 changed files with 73 additions and 28 deletions

No files matched your search

+18 -15
View File
@@ -35,24 +35,27 @@ function computeTimelineStateColor(
const domain = computeDomain(stateObj.entity_id);
// Zone states for person/device_tracker don't have specific CSS color variables,
// so they all fall back to the same --state-person-active-color.
// Only use a custom CSS variable if explicitly defined (e.g. --state-person-kitchen-color),
// otherwise return undefined to get unique colors from the generic color handler.
if (
// Zone states would all share --state-person-active-color, so only an
// exact state variable may color them, like states of uncolored domains.
const isZoneState =
(domain === "person" || domain === "device_tracker") &&
!((FIXED_DOMAIN_STATES[domain] || []) as readonly string[]).includes(state)
) {
return computeCssValue(
`--state-${domain}-${slugify(state, "_")}-color`,
computedStyles
);
}
const properties = stateColorProperties(stateObj, state);
!((FIXED_DOMAIN_STATES[domain] || []) as readonly string[]).includes(state);
const properties = isZoneState
? undefined
: stateColorProperties(stateObj, state);
if (!properties) {
return undefined;
const stateKey = slugify(state, "_");
const deviceClass = stateObj.attributes.device_class;
return computeCssValue(
[
...(deviceClass
? [`--state-${domain}-${deviceClass}-${stateKey}-color`]
: []),
`--state-${domain}-${stateKey}-color`,
],
computedStyles
);
}
const rgb = computeCssValue(properties, computedStyles);
+7 -13
View File
@@ -364,25 +364,19 @@ const getWeatherExtrema = (
if (new Date(fc.datetime).getDate() !== today) {
break;
}
if (fc.temperature == null) {
continue;
}
if (tempHigh === undefined || fc.temperature > tempHigh) {
if (!tempHigh || fc.temperature > tempHigh) {
tempHigh =
temperatureFractionDigits === undefined
? fc.temperature
: round(fc.temperature, temperatureFractionDigits);
}
if (fc.templow != null && (tempLow === undefined || fc.templow < tempLow)) {
if (fc.templow !== undefined && (!tempLow || fc.templow < tempLow)) {
tempLow =
temperatureFractionDigits === undefined
? fc.templow
: round(fc.templow, temperatureFractionDigits);
}
if (
fc.templow == null &&
(tempLow === undefined || fc.temperature < tempLow)
) {
if (!fc.templow && (!tempLow || fc.temperature < tempLow)) {
tempLow =
temperatureFractionDigits === undefined
? fc.temperature
@@ -390,19 +384,19 @@ const getWeatherExtrema = (
}
}
if (tempLow === undefined && tempHigh === undefined) {
if (!tempLow && !tempHigh) {
return undefined;
}
return html`
${
tempHigh !== undefined
tempHigh
? formatEntityAttributeValue(stateObj, "temperature", tempHigh)
: ""
}
${tempLow !== undefined && tempHigh !== undefined ? " / " : ""}
${tempLow && tempHigh ? " / " : ""}
${
tempLow !== undefined
tempLow
? formatEntityAttributeValue(stateObj, "temperature", tempLow)
: ""
}
@@ -0,0 +1,48 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { describe, expect, it } from "vitest";
import { computeTimelineColor } from "../../../src/components/chart/timeline-color";
import { createMockComputedStyle } from "../../fixtures/computed-style";
const entity = (
entity_id: string,
attributes: Record<string, unknown> = {}
): HassEntity =>
({
entity_id,
state: "",
attributes,
last_changed: "",
last_updated: "",
context: { id: "", parent_id: null, user_id: null },
}) as HassEntity;
describe("computeTimelineColor", () => {
it("uses a themed color for a custom state of an uncolored domain", () => {
const styles = createMockComputedStyle({
"--state-sensor-heating-color": "#ff0000",
"--state-sensor-power-heating-color": "#00ff00",
});
expect(
computeTimelineColor("Heating", styles, entity("sensor.heat_pump"))
).toBe("#ff0000");
expect(
computeTimelineColor(
"Heating",
styles,
entity("sensor.heat_pump", { device_class: "power" })
)
).toBe("#00ff00");
});
it("keeps active/inactive fallbacks away from uncolored domains", () => {
const styles = createMockComputedStyle({
"--state-active-color": "#123456",
"--state-sensor-active-color": "#654321",
});
expect(
computeTimelineColor("Heating", styles, entity("sensor.heat_pump"))
).not.toMatch(/^#(123456|654321)$/);
});
});