Skip to content

Commit

Permalink
Use sensor device class for graph and precision (#18099)
Browse files Browse the repository at this point in the history
Co-authored-by: Bram Kragten <[email protected]>
  • Loading branch information
piitaya and bramkragten authored Oct 23, 2023
1 parent c6be4d6 commit aeaf091
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 40 deletions.
54 changes: 39 additions & 15 deletions src/data/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand All @@ -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) {
Expand Down
18 changes: 18 additions & 0 deletions src/data/sensor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SensorNumericDeviceClasses> => {
if (sensorNumericDeviceClassesCache) {
return sensorNumericDeviceClassesCache;
}
sensorNumericDeviceClassesCache = await hass.callWS({
type: "sensor/numeric_device_classes",
});
return sensorNumericDeviceClassesCache!;
};
22 changes: 14 additions & 8 deletions src/dialogs/more-info/ha-more-info-history.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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) => {
Expand All @@ -223,7 +228,8 @@ export class MoreInfoHistory extends LitElement {
this._stateHistory = computeHistory(
this.hass!,
combinedHistory,
this.hass!.localize
this.hass!.localize,
sensorNumericDeviceClasses
);
},
24,
Expand Down
27 changes: 19 additions & 8 deletions src/panels/config/entities/entity-registry-settings-editor.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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";
Expand All @@ -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";
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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: [
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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`
Expand Down
11 changes: 8 additions & 3 deletions src/panels/history/ha-panel-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -306,14 +307,18 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {

const now = new Date();

const { numeric_device_classes: sensorNumericDeviceClasses } =
await getSensorNumericDeviceClasses(this.hass);

this._subscribed = subscribeHistory(
this.hass,
(history) => {
this._isLoading = false;
this._stateHistory = computeHistory(
this.hass,
history,
this.hass.localize
this.hass.localize,
sensorNumericDeviceClasses
);
},
this._startDate,
Expand Down
19 changes: 13 additions & 6 deletions src/panels/lovelace/cards/hui-history-graph-card.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -97,21 +98,27 @@ 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) => {
if (!this._subscribed) {
// 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,
Expand Down

0 comments on commit aeaf091

Please sign in to comment.