diff --git a/src/common/entity/state_active.ts b/src/common/entity/state_active.ts index 78f94e8e59..6be6728c20 100644 --- a/src/common/entity/state_active.ts +++ b/src/common/entity/state_active.ts @@ -5,14 +5,14 @@ import { computeDomain } from "./compute_domain"; const NORMAL_UNKNOWN_DOMAIN = ["button", "input_button", "scene"]; const NORMAL_OFF_DOMAIN = ["script"]; -export function stateActive(stateObj: HassEntity): boolean { +export function stateActive(stateObj: HassEntity, state?: string): boolean { const domain = computeDomain(stateObj.entity_id); - const state = stateObj.state; + const compareState = state !== undefined ? state : stateObj?.state; if ( - OFF_STATES.includes(state) && - !(NORMAL_UNKNOWN_DOMAIN.includes(domain) && state === "unknown") && - !(NORMAL_OFF_DOMAIN.includes(domain) && state === "script") + OFF_STATES.includes(compareState) && + !(NORMAL_UNKNOWN_DOMAIN.includes(domain) && compareState === "unknown") && + !(NORMAL_OFF_DOMAIN.includes(domain) && compareState === "script") ) { return false; } @@ -20,16 +20,16 @@ export function stateActive(stateObj: HassEntity): boolean { // Custom cases switch (domain) { case "cover": - return state === "open" || state === "opening"; + return compareState === "open" || compareState === "opening"; case "device_tracker": case "person": - return state !== "not_home"; + return compareState !== "not_home"; case "media-player": - return state !== "idle" && state !== "standby"; + return compareState !== "idle" && compareState !== "standby"; case "vacuum": - return state === "on" || state === "cleaning"; + return compareState === "on" || compareState === "cleaning"; case "plant": - return state === "problem"; + return compareState === "problem"; default: return true; } diff --git a/src/common/entity/state_color.ts b/src/common/entity/state_color.ts index cec598d598..eb88e65673 100644 --- a/src/common/entity/state_color.ts +++ b/src/common/entity/state_color.ts @@ -10,12 +10,12 @@ 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)) { +export const stateColorCss = (stateObj?: HassEntity, state?: string) => { + if (!stateObj || !stateActive(stateObj, state)) { return `var(--rgb-disabled-color)`; } - const color = stateColor(stateObj); + const color = stateColor(stateObj, state); if (color) { return `var(--rgb-state-${color}-color)`; @@ -24,13 +24,13 @@ export const stateColorCss = (stateObj?: HassEntity) => { return `var(--rgb-primary-color)`; }; -export const stateColor = (stateObj: HassEntity) => { - const state = stateObj.state; +export const stateColor = (stateObj: HassEntity, state?: string) => { + const compareState = state !== undefined ? state : stateObj?.state; const domain = computeDomain(stateObj.entity_id); switch (domain) { case "alarm_control_panel": - return alarmControlPanelColor(state); + return alarmControlPanelColor(compareState); case "binary_sensor": return binarySensorColor(stateObj); @@ -39,10 +39,10 @@ export const stateColor = (stateObj: HassEntity) => { return coverColor(stateObj); case "climate": - return climateColor(state); + return climateColor(compareState); case "lock": - return lockColor(state); + return lockColor(compareState); case "light": return "light"; @@ -64,7 +64,7 @@ export const stateColor = (stateObj: HassEntity) => { return "vacuum"; case "sun": - return state === "above_horizon" ? "sun-day" : "sun-night"; + return compareState === "above_horizon" ? "sun-day" : "sun-night"; case "update": return updateIsInstalling(stateObj as UpdateEntity) diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts index cc0bfbe93d..538afd6b34 100644 --- a/src/components/chart/state-history-chart-timeline.ts +++ b/src/components/chart/state-history-chart-timeline.ts @@ -3,8 +3,10 @@ import { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; import { getGraphColorByIndex } from "../../common/color/colors"; +import { rgb2hex } from "../../common/color/convert-color"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; -import { computeDomain } from "../../common/entity/compute_domain"; +import { stateActive } from "../../common/entity/state_active"; +import { stateColor } from "../../common/entity/state_color"; import { numberFormatToLocale } from "../../common/number/format_number"; import { computeRTL } from "../../common/util/compute_rtl"; import { TimelineEntity } from "../../data/history"; @@ -12,65 +14,55 @@ import { HomeAssistant } from "../../types"; import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; import type { TimeLineData } from "./timeline-chart/const"; -/** Binary sensor device classes for which the static colors for on/off are NOT inverted. - * List the ones were "on" = good or normal state => should be rendered "green". - * Note: It is now a "not inverted" list (compared to the past) since we now have more inverted ones. - */ -const BINARY_SENSOR_DEVICE_CLASS_COLOR_NOT_INVERTED = new Set([ - "battery_charging", - "connectivity", - "light", - "moving", - "plug", - "power", - "presence", - "running", -]); - -const STATIC_STATE_COLORS = new Set([ - "on", - "off", - "home", - "not_home", - "unavailable", - "unknown", - "idle", -]); - +const stateColorTokenMap: Map = new Map(); const stateColorMap: Map = new Map(); let colorIndex = 0; -const invertOnOff = (entityState?: HassEntity) => - entityState && - computeDomain(entityState.entity_id) === "binary_sensor" && - "device_class" in entityState.attributes && - !BINARY_SENSOR_DEVICE_CLASS_COLOR_NOT_INVERTED.has( - entityState.attributes.device_class! - ); +export const getStateColorToken = ( + stateString: string, + entityState: HassEntity +) => { + if (!stateActive(entityState, stateString)) { + return `disabled`; + } + const color = stateColor(entityState, stateString); + if (color) { + return `state-${color}`; + } + return undefined; +}; const getColor = ( stateString: string, entityState: HassEntity, computedStyles: CSSStyleDeclaration ) => { - // Inversion is only valid for "on" or "off" state - if ( - (stateString === "on" || stateString === "off") && - invertOnOff(entityState) - ) { - stateString = stateString === "on" ? "off" : "on"; + const stateColorToken = getStateColorToken(stateString, entityState); + + if (stateColorToken) { + if (stateColorTokenMap.has(stateColorToken)) { + return stateColorTokenMap.get(stateColorToken); + } + const value = computedStyles.getPropertyValue( + `--rgb-${stateColorToken}-color` + ); + + if (value) { + const parsedValue = value.split(",").map((v) => Number(v)) as [ + number, + number, + number + ]; + const hexValue = rgb2hex(parsedValue); + stateColorTokenMap.set(stateColorToken, hexValue); + return hexValue; + } } + if (stateColorMap.has(stateString)) { return stateColorMap.get(stateString); } - if (STATIC_STATE_COLORS.has(stateString)) { - const color = computedStyles.getPropertyValue( - `--state-${stateString}-color` - ); - stateColorMap.set(stateString, color); - return color; - } const color = getGraphColorByIndex(colorIndex, computedStyles); colorIndex++; stateColorMap.set(stateString, color); diff --git a/src/dialogs/more-info/ha-more-info-logbook.ts b/src/dialogs/more-info/ha-more-info-logbook.ts index 4777ad64a8..0698489112 100644 --- a/src/dialogs/more-info/ha-more-info-logbook.ts +++ b/src/dialogs/more-info/ha-more-info-logbook.ts @@ -45,6 +45,7 @@ export class MoreInfoLogbook extends LitElement { narrow no-icon no-name + show-indicator relative-time > `; diff --git a/src/panels/logbook/ha-logbook-renderer.ts b/src/panels/logbook/ha-logbook-renderer.ts index 74321e70bc..adf30e47f5 100644 --- a/src/panels/logbook/ha-logbook-renderer.ts +++ b/src/panels/logbook/ha-logbook-renderer.ts @@ -11,12 +11,15 @@ import { import type { HassEntity } from "home-assistant-js-websocket"; import { customElement, eventOptions, property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; +import { styleMap } from "lit/directives/style-map"; import { formatDate } from "../../common/datetime/format_date"; import { formatTimeWithSeconds } from "../../common/datetime/format_time"; import { restoreScroll } from "../../common/decorators/restore-scroll"; import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; +import { stateActive } from "../../common/entity/state_active"; +import { stateColorCss } from "../../common/entity/state_color"; import "../../components/entity/state-badge"; import "../../components/ha-circular-progress"; import "../../components/ha-relative-time"; @@ -67,6 +70,9 @@ class HaLogbookRenderer extends LitElement { @property({ type: Boolean, attribute: "virtualize", reflect: true }) public virtualize = false; + @property({ type: Boolean, attribute: "show-indicator" }) + public showIndicator = false; + @property({ type: Boolean, attribute: "no-icon" }) public noIcon = false; @@ -132,7 +138,7 @@ class HaLogbookRenderer extends LitElement { if (!item || index === undefined) { return html``; } - const previous = this.entries[index - 1]; + const previous = this.entries[index - 1] as LogbookEntry | undefined; const seenEntityIds: string[] = []; const currentStateObj = item.entity_id ? this.hass.states[item.entity_id] @@ -199,6 +205,7 @@ class HaLogbookRenderer extends LitElement { > ` : ""} + ${this.showIndicator ? this._renderIndicator(item) : ""}
${!this.noName // Used for more-info panel (single entity case) @@ -253,6 +260,23 @@ class HaLogbookRenderer extends LitElement { }); } + private _renderIndicator(item: LogbookEntry) { + const stateObj = this.hass.states[item.entity_id!] as + | HassEntity + | undefined; + + const color = + stateObj && stateActive(stateObj, item.state) + ? stateColorCss(stateObj, item.state) + : undefined; + + const style = { + "--indicator-color": color, + }; + + return html`
`; + } + private _renderMessage( item: LogbookEntry, seenEntityIds: string[], @@ -541,6 +565,7 @@ class HaLogbookRenderer extends LitElement { } .entry { + position: relative; display: flex; width: 100%; line-height: 2em; @@ -551,6 +576,20 @@ class HaLogbookRenderer extends LitElement { align-items: center; } + .indicator { + background-color: rgb( + var(--indicator-color, var(--rgb-disabled-color)) + ); + height: 8px; + width: 8px; + border-radius: 4px; + flex-shrink: 0; + margin-right: 12px; + margin-inline-start: initial; + margin-inline-end: 12px; + direction: var(--direction); + } + ha-icon-next { color: var(--secondary-text-color); } diff --git a/src/panels/logbook/ha-logbook.ts b/src/panels/logbook/ha-logbook.ts index 57ae5ac00a..025d8369ce 100644 --- a/src/panels/logbook/ha-logbook.ts +++ b/src/panels/logbook/ha-logbook.ts @@ -65,6 +65,9 @@ export class HaLogbook extends LitElement { @property({ type: Boolean, attribute: "no-name" }) public noName = false; + @property({ type: Boolean, attribute: "show-indicator" }) + public showIndicator = false; + @property({ type: Boolean, attribute: "relative-time" }) public relativeTime = false; @@ -126,6 +129,7 @@ export class HaLogbook extends LitElement { .virtualize=${this.virtualize} .noIcon=${this.noIcon} .noName=${this.noName} + .showIndicator=${this.showIndicator} .relativeTime=${this.relativeTime} .entries=${this._logbookEntries} .traceContexts=${this._traceContexts}