From 9b7db191a66a3e95c3f0a256f6f091005a6116c3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 22 May 2025 11:32:44 +0200 Subject: [PATCH] Add range support to icon translations (#25541) Co-authored-by: Bram Kragten --- src/common/entity/state_icon.ts | 8 --- src/data/icons.ts | 88 ++++++++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 14 deletions(-) diff --git a/src/common/entity/state_icon.ts b/src/common/entity/state_icon.ts index 58a63e881e..6eaef31725 100644 --- a/src/common/entity/state_icon.ts +++ b/src/common/entity/state_icon.ts @@ -2,7 +2,6 @@ import type { HassEntity } from "home-assistant-js-websocket"; import { computeStateDomain } from "./compute_state_domain"; import { updateIcon } from "./update_icon"; import { deviceTrackerIcon } from "./device_tracker_icon"; -import { batteryIcon } from "./battery_icon"; export const stateIcon = ( stateObj: HassEntity, @@ -10,17 +9,10 @@ export const stateIcon = ( ): string | undefined => { const domain = computeStateDomain(stateObj); const compareState = state ?? stateObj.state; - const dc = stateObj.attributes.device_class; switch (domain) { case "update": return updateIcon(stateObj, compareState); - case "sensor": - if (dc === "battery") { - return batteryIcon(stateObj, compareState); - } - break; - case "device_tracker": return deviceTrackerIcon(stateObj, compareState); diff --git a/src/data/icons.ts b/src/data/icons.ts index c59224b2a9..93cd21fee1 100644 --- a/src/data/icons.ts +++ b/src/data/icons.ts @@ -145,10 +145,12 @@ type PlatformIcons = Record< string, { state: Record; + range?: Record; state_attributes: Record< string, { state: Record; + range?: Record; default: string; } >; @@ -160,10 +162,12 @@ export type ComponentIcons = Record< string, { state?: Record; + range?: Record; state_attributes?: Record< string, { state: Record; + range?: Record; default: string; } >; @@ -286,6 +290,74 @@ export const getServiceIcons = async ( return resources.services.domains[domain]; }; +// Cache for sorted range keys +const sortedRangeCache = new WeakMap, number[]>(); + +// Helper function to get an icon from a range of values +const getIconFromRange = ( + value: number, + range: Record +): string | undefined => { + // Get cached range values or compute and cache them + let rangeValues = sortedRangeCache.get(range); + if (!rangeValues) { + rangeValues = Object.keys(range) + .map(Number) + .filter((k) => !isNaN(k)) + .sort((a, b) => a - b); + sortedRangeCache.set(range, rangeValues); + } + + if (rangeValues.length === 0) { + return undefined; + } + + // If the value is below the first threshold, return undefined + // (we'll fall back to the default icon) + if (value < rangeValues[0]) { + return undefined; + } + + // Find the highest threshold that's less than or equal to the value + let selectedThreshold = rangeValues[0]; + for (const threshold of rangeValues) { + if (value >= threshold) { + selectedThreshold = threshold; + } else { + break; + } + } + + return range[selectedThreshold.toString()]; +}; + +// Helper function to get an icon based on state and translations +const getIconFromTranslations = ( + state: string | number | undefined, + translations: + | { + default?: string; + state?: Record; + range?: Record; + } + | undefined +): string | undefined => { + if (!translations) { + return undefined; + } + + // First check for exact state match + if (state && translations.state?.[state]) { + return translations.state[state]; + } + // Then check for range-based icons if we have a numeric state + if (state !== undefined && translations.range && !isNaN(Number(state))) { + return getIconFromRange(Number(state), translations.range); + } + // Fallback to default icon + return translations.default; +}; + export const entityIcon = async ( hass: HomeAssistant, stateObj: HassEntity, @@ -331,7 +403,8 @@ const getEntityIcon = async ( const platformIcons = await getPlatformIcons(hass, platform); if (platformIcons) { const translations = platformIcons[domain]?.[translation_key]; - icon = (state && translations?.state?.[state]) || translations?.default; + + icon = getIconFromTranslations(state, translations); } } @@ -345,7 +418,8 @@ const getEntityIcon = async ( const translations = (device_class && entityComponentIcons[device_class]) || entityComponentIcons._; - icon = (state && translations?.state?.[state]) || translations?.default; + + icon = getIconFromTranslations(state, translations); } } return icon; @@ -372,9 +446,10 @@ export const attributeIcon = async ( if (translation_key && platform) { const platformIcons = await getPlatformIcons(hass, platform); if (platformIcons) { - const translations = - platformIcons[domain]?.[translation_key]?.state_attributes?.[attribute]; - icon = (value && translations?.state?.[value]) || translations?.default; + icon = getIconFromTranslations( + value, + platformIcons[domain]?.[translation_key]?.state_attributes?.[attribute] + ); } } if (!icon) { @@ -384,7 +459,8 @@ export const attributeIcon = async ( (deviceClass && entityComponentIcons[deviceClass]?.state_attributes?.[attribute]) || entityComponentIcons._?.state_attributes?.[attribute]; - icon = (value && translations?.state?.[value]) || translations?.default; + + icon = getIconFromTranslations(value, translations); } } return icon;