diff --git a/src/data/history.ts b/src/data/history.ts index 653dbef356e4..89b15ef75a52 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -395,16 +395,28 @@ const processLineChartEntities = ( }; }; -const stateUsesUnits = (state: HassEntity) => - attributesHaveUnits(state.attributes); +const NUMERICAL_DOMAINS = ["counter", "input_number", "number"]; -const attributesHaveUnits = (attributes: { [key: string]: any }) => +const isNumericFromDomain = (domain: string) => + NUMERICAL_DOMAINS.includes(domain); + +const isNumericFromAttributes = (attributes: { [key: string]: any }) => "unit_of_measurement" in attributes || "state_class" in attributes; +const isNumericSensorEntity = ( + stateObj: HassEntity, + sensorNumericalDeviceClasses: string[] +) => + stateObj.attributes.device_class != null && + sensorNumericalDeviceClasses.includes(stateObj.attributes.device_class); + +const BLANK_UNIT = " "; + export const computeHistory = ( hass: HomeAssistant, stateHistory: HistoryStates, - localize: LocalizeFunc + localize: LocalizeFunc, + sensorNumericalDeviceClasses: string[] ): HistoryResult => { const lineChartDevices: { [unit: string]: HistoryStates } = {}; const timelineDevices: TimelineEntity[] = []; @@ -417,28 +429,40 @@ export const computeHistory = ( return; } + const domain = computeDomain(entityId); + const currentState = entityId in hass.states ? hass.states[entityId] : undefined; - const stateWithUnitorStateClass = - !currentState && - stateInfo.find((state) => state.a && attributesHaveUnits(state.a)); + const numericStateFromHistory = + currentState || isNumericFromDomain(domain) + ? undefined + : stateInfo.find( + (state) => state.a && isNumericFromAttributes(state.a) + ); let unit: string | undefined; - if (currentState && stateUsesUnits(currentState)) { - unit = currentState.attributes.unit_of_measurement || " "; - } else if (stateWithUnitorStateClass) { - unit = stateWithUnitorStateClass.a.unit_of_measurement || " "; + const isNumeric = + isNumericFromDomain(domain) || + (currentState != null && + isNumericFromAttributes(currentState.attributes)) || + (currentState != null && + domain === "sensor" && + isNumericSensorEntity(currentState, sensorNumericalDeviceClasses)) || + numericStateFromHistory != null; + + if (isNumeric) { + unit = + currentState?.attributes.unit_of_measurement || + numericStateFromHistory?.a.unit_of_measurement || + BLANK_UNIT; } else { unit = { zone: localize("ui.dialogs.more_info_control.zone.graph_unit"), climate: hass.config.unit_system.temperature, - counter: "#", humidifier: "%", - input_number: "#", - number: "#", water_heater: hass.config.unit_system.temperature, - }[computeDomain(entityId)]; + }[domain]; } if (!unit) { diff --git a/src/data/sensor.ts b/src/data/sensor.ts index 6f471b6930d4..903471584866 100644 --- a/src/data/sensor.ts +++ b/src/data/sensor.ts @@ -13,3 +13,21 @@ export const getSensorDeviceClassConvertibleUnits = ( type: "sensor/device_class_convertible_units", device_class: deviceClass, }); + +export type SensorNumericDeviceClasses = { + numeric_device_classes: string[]; +}; + +let sensorNumericDeviceClassesCache: SensorNumericDeviceClasses | undefined; + +export const getSensorNumericDeviceClasses = async ( + hass: HomeAssistant +): Promise => { + if (sensorNumericDeviceClassesCache) { + return sensorNumericDeviceClassesCache; + } + sensorNumericDeviceClassesCache = await hass.callWS({ + type: "sensor/numeric_device_classes", + }); + return sensorNumericDeviceClassesCache!; +}; diff --git a/src/dialogs/more-info/ha-more-info-history.ts b/src/dialogs/more-info/ha-more-info-history.ts index 47593f49fec0..9953d319a0c5 100644 --- a/src/dialogs/more-info/ha-more-info-history.ts +++ b/src/dialogs/more-info/ha-more-info-history.ts @@ -1,28 +1,29 @@ import { startOfYesterday, subHours } from "date-fns/esm"; -import { css, html, LitElement, PropertyValues, nothing } from "lit"; -import { customElement, property, state, query } from "lit/decorators"; +import { LitElement, PropertyValues, css, html, nothing } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; import { createSearchParam } from "../../common/url/search-params"; +import { ChartResizeOptions } from "../../components/chart/ha-chart-base"; import "../../components/chart/state-history-charts"; import type { StateHistoryCharts } from "../../components/chart/state-history-charts"; import "../../components/chart/statistics-chart"; +import type { StatisticsChart } from "../../components/chart/statistics-chart"; import { - computeHistory, HistoryResult, + computeHistory, subscribeHistoryStatesTimeWindow, } from "../../data/history"; import { - fetchStatistics, - getStatisticMetadata, Statistics, StatisticsMetaData, StatisticsTypes, + fetchStatistics, + getStatisticMetadata, } from "../../data/recorder"; +import { getSensorNumericDeviceClasses } from "../../data/sensor"; import { HomeAssistant } from "../../types"; -import type { StatisticsChart } from "../../components/chart/statistics-chart"; -import { ChartResizeOptions } from "../../components/chart/ha-chart-base"; declare global { interface HASSDomEvents { @@ -213,6 +214,10 @@ export class MoreInfoHistory extends LitElement { if (this._subscribed) { this._unsubscribeHistory(); } + + const { numeric_device_classes: sensorNumericDeviceClasses } = + await getSensorNumericDeviceClasses(this.hass); + this._subscribed = subscribeHistoryStatesTimeWindow( this.hass!, (combinedHistory) => { @@ -223,7 +228,8 @@ export class MoreInfoHistory extends LitElement { this._stateHistory = computeHistory( this.hass!, combinedHistory, - this.hass!.localize + this.hass!.localize, + sensorNumericDeviceClasses ); }, 24, diff --git a/src/panels/config/entities/entity-registry-settings-editor.ts b/src/panels/config/entities/entity-registry-settings-editor.ts index c5c19dbee068..42c64edd64df 100644 --- a/src/panels/config/entities/entity-registry-settings-editor.ts +++ b/src/panels/config/entities/entity-registry-settings-editor.ts @@ -1,5 +1,6 @@ import "@material/mwc-button/mwc-button"; import "@material/mwc-formfield/mwc-formfield"; +import { mdiContentCopy } from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; import { css, @@ -11,7 +12,6 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { mdiContentCopy } from "@mdi/js"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { fireEvent } from "../../../common/dom/fire_event"; import { stopPropagation } from "../../../common/dom/stop_propagation"; @@ -25,6 +25,7 @@ import { LocalizeFunc, LocalizeKeys, } from "../../../common/translations/localize"; +import { copyToClipboard } from "../../../common/util/copy-clipboard"; import "../../../components/ha-alert"; import "../../../components/ha-area-picker"; import "../../../components/ha-icon"; @@ -38,9 +39,9 @@ import "../../../components/ha-switch"; import type { HaSwitch } from "../../../components/ha-switch"; import "../../../components/ha-textfield"; import { - CameraPreferences, CAMERA_ORIENTATIONS, CAMERA_SUPPORT_STREAM, + CameraPreferences, fetchCameraPrefs, STREAM_TYPE_HLS, updateCameraPrefs, @@ -66,7 +67,10 @@ import { } from "../../../data/entity_registry"; import { domainToName } from "../../../data/integration"; import { getNumberDeviceClassConvertibleUnits } from "../../../data/number"; -import { getSensorDeviceClassConvertibleUnits } from "../../../data/sensor"; +import { + getSensorDeviceClassConvertibleUnits, + getSensorNumericDeviceClasses, +} from "../../../data/sensor"; import { getWeatherConvertibleUnits, WeatherUnits, @@ -80,9 +84,8 @@ import { showVoiceAssistantsView } from "../../../dialogs/more-info/components/v import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; -import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; -import { copyToClipboard } from "../../../common/util/copy-clipboard"; import { showToast } from "../../../util/toast"; +import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; const OVERRIDE_DEVICE_CLASSES = { cover: [ @@ -174,6 +177,8 @@ export class EntityRegistrySettingsEditor extends LitElement { @state() private _sensorDeviceClassConvertibleUnits?: string[]; + @state() private _sensorNumericalDeviceClasses?: string[]; + @state() private _weatherConvertibleUnits?: WeatherUnits; @state() private _defaultCode?: string | null; @@ -195,8 +200,6 @@ export class EntityRegistrySettingsEditor extends LitElement { this._name = this.entry.name || ""; this._icon = this.entry.icon || ""; - this._deviceClass = - this.entry.device_class || this.entry.original_device_class; this._origEntityId = this.entry.entity_id; this._areaId = this.entry.area_id; this._entityId = this.entry.entity_id; @@ -294,6 +297,14 @@ export class EntityRegistrySettingsEditor extends LitElement { } else { this._numberDeviceClassConvertibleUnits = []; } + if (domain === "sensor") { + const { numeric_device_classes } = await getSensorNumericDeviceClasses( + this.hass + ); + this._sensorNumericalDeviceClasses = numeric_device_classes; + } else { + this._sensorNumericalDeviceClasses = []; + } if (domain === "sensor" && this._deviceClass) { const { units } = await getSensorDeviceClassConvertibleUnits( this.hass, @@ -558,7 +569,7 @@ export class EntityRegistrySettingsEditor extends LitElement { // Allow customizing the precision for a sensor with numerical device class, // a unit of measurement or state class ((this._deviceClass && - !["date", "enum", "timestamp"].includes(this._deviceClass)) || + this._sensorNumericalDeviceClasses?.includes(this._deviceClass)) || stateObj?.attributes.unit_of_measurement || stateObj?.attributes.state_class) ? html` diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts index 94d5ab6c159f..0535e439ef15 100644 --- a/src/panels/history/ha-panel-history.ts +++ b/src/panels/history/ha-panel-history.ts @@ -4,7 +4,7 @@ import { HassServiceTarget, UnsubscribeFunc, } from "home-assistant-js-websocket/dist/types"; -import { css, html, LitElement, PropertyValues } from "lit"; +import { LitElement, PropertyValues, css, html } from "lit"; import { property, query, state } from "lit/decorators"; import { ensureArray } from "../../common/array/ensure-array"; import { storage } from "../../common/decorators/storage"; @@ -39,10 +39,11 @@ import { } from "../../data/device_registry"; import { subscribeEntityRegistry } from "../../data/entity_registry"; import { - computeHistory, HistoryResult, + computeHistory, subscribeHistory, } from "../../data/history"; +import { getSensorNumericDeviceClasses } from "../../data/sensor"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { haStyle } from "../../resources/styles"; import { HomeAssistant } from "../../types"; @@ -306,6 +307,9 @@ class HaPanelHistory extends SubscribeMixin(LitElement) { const now = new Date(); + const { numeric_device_classes: sensorNumericDeviceClasses } = + await getSensorNumericDeviceClasses(this.hass); + this._subscribed = subscribeHistory( this.hass, (history) => { @@ -313,7 +317,8 @@ class HaPanelHistory extends SubscribeMixin(LitElement) { this._stateHistory = computeHistory( this.hass, history, - this.hass.localize + this.hass.localize, + sensorNumericDeviceClasses ); }, this._startDate, diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index 5219027fc9fe..7c4ad061a763 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -1,22 +1,23 @@ import { - css, CSSResultGroup, - html, LitElement, PropertyValues, + css, + html, nothing, } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import "../../../components/chart/state-history-charts"; -import "../../../components/ha-card"; import "../../../components/ha-alert"; +import "../../../components/ha-card"; import { - computeHistory, HistoryResult, + computeHistory, subscribeHistoryStatesTimeWindow, } from "../../../data/history"; +import { getSensorNumericDeviceClasses } from "../../../data/sensor"; import { HomeAssistant } from "../../../types"; import { hasConfigOrEntitiesChanged } from "../common/has-changed"; import { processConfigEntities } from "../common/process-config-entities"; @@ -97,10 +98,14 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { this._unsubscribeHistory(); } - private _subscribeHistory() { + private async _subscribeHistory() { if (!isComponentLoaded(this.hass!, "history") || this._subscribed) { return; } + + const { numeric_device_classes: sensorNumericDeviceClasses } = + await getSensorNumericDeviceClasses(this.hass!); + this._subscribed = subscribeHistoryStatesTimeWindow( this.hass!, (combinedHistory) => { @@ -108,10 +113,12 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { // Message came in before we had a chance to unload return; } + this._stateHistory = computeHistory( this.hass!, combinedHistory, - this.hass!.localize + this.hass!.localize, + sensorNumericDeviceClasses ); }, this._hoursToShow,