Compare commits

...

1 Commits

Author SHA1 Message Date
Paul Bottein 039dc59d1d Fix flash of unformatted entity states on first load 2026-06-15 22:36:24 +02:00
25 changed files with 226 additions and 456 deletions
@@ -0,0 +1,51 @@
name: Sync numeric device classes
# Mirrors Home Assistant Core's numeric `SensorDeviceClass` list into the
# build-time default in src/data/sensor_numeric_device_classes.ts and opens a PR
# when it drifts. Reads homeassistant/generated/sensor.json from core.
on:
workflow_dispatch:
schedule:
- cron: "0 4 * * *" # Daily, 04:00 UTC
permissions:
contents: read
jobs:
sync:
name: Sync
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout the repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
- name: Regenerate numeric device classes
run: ./script/gen_numeric_device_classes
- name: Format
run: yarn prettier --write src/data/sensor_numeric_device_classes.ts
- name: Create pull request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
branch: chore/sync-numeric-device-classes
commit-message: Update numeric sensor device classes
title: Update numeric sensor device classes
body: |
Regenerated `SENSOR_NUMERIC_DEVICE_CLASSES` from Home Assistant Core's
`SensorDeviceClass`.
Automated by `.github/workflows/sync-numeric-device-classes.yaml`.
@@ -0,0 +1,44 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import process from "node:process";
import gulp from "gulp";
import paths from "../paths.cjs";
// Numeric sensor device classes are owned by Home Assistant Core
// (`SensorDeviceClass` minus `NON_NUMERIC_DEVICE_CLASSES`). Core publishes the
// canonical list as a generated JSON; mirror it into a dedicated file so entity
// states render formatted from the first frame.
const SOURCE_URL =
process.env.SENSOR_METADATA_URL ||
"https://raw.githubusercontent.com/home-assistant/core/dev/homeassistant/generated/sensor.json";
const TARGET = join(
paths.root_dir,
"src",
"data",
"sensor_numeric_device_classes.ts"
);
gulp.task("gen-numeric-device-classes", async () => {
const response = await fetch(SOURCE_URL);
if (!response.ok) {
throw new Error(`Failed to fetch ${SOURCE_URL}: ${response.status}`);
}
const data = await response.json();
const classes = [...(data.numeric_device_classes ?? [])].sort();
if (!classes.length) {
throw new Error(`No numeric_device_classes found in ${SOURCE_URL}`);
}
const content = `// This file is auto-generated from Home Assistant Core's \`SensorDeviceClass\`
// (all values minus \`NON_NUMERIC_DEVICE_CLASSES\`). Do not edit by hand.
// Regenerate with \`script/gen_numeric_device_classes\`.
export const SENSOR_NUMERIC_DEVICE_CLASSES: string[] = [
${classes.map((deviceClass) => ` "${deviceClass}",`).join("\n")}
];
`;
await writeFile(TARGET, content);
});
+1
View File
@@ -9,6 +9,7 @@ import "./fetch-nightly-translations.js";
import "./gallery.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./gen-numeric-device-classes.js";
import "./landing-page.js";
import "./locale-data.js";
import "./rspack.js";
-1
View File
@@ -372,7 +372,6 @@ export class DemoEntityState extends LitElement {
hass.localize,
entry.stateObj,
hass.locale,
[], // numericDeviceClasses
hass.config,
hass.entities
)}`,
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Safe bash settings
# -e Exit on command fail
# -u Exit on unset variable
# -o pipefail Exit if piped command has error code
set -eu -o pipefail
cd "$(dirname "$0")/.."
./node_modules/.bin/gulp gen-numeric-device-classes
+2 -8
View File
@@ -18,6 +18,7 @@ import { blankBeforeUnit } from "../translations/blank_before_unit";
import type { LocalizeFunc } from "../translations/localize";
import { computeDomain } from "./compute_domain";
import { SENSOR_TIMESTAMP_DEVICE_CLASSES } from "../../data/sensor";
import { SENSOR_NUMERIC_DEVICE_CLASSES } from "../../data/sensor_numeric_device_classes";
// Domains whose state is a timezone-agnostic date and/or time string.
const DATE_TIME_DOMAINS = new Set(["date", "input_datetime", "time"]);
@@ -57,7 +58,6 @@ export const computeStateDisplay = (
localize: LocalizeFunc,
stateObj: HassEntity,
locale: FrontendLocaleData,
sensorNumericDeviceClasses: string[],
config: HassConfig,
entities: HomeAssistant["entities"],
state?: string
@@ -68,7 +68,6 @@ export const computeStateDisplay = (
return computeStateDisplayFromEntityAttributes(
localize,
locale,
sensorNumericDeviceClasses,
config,
entity,
stateObj.entity_id,
@@ -80,7 +79,6 @@ export const computeStateDisplay = (
export const computeStateDisplayFromEntityAttributes = (
localize: LocalizeFunc,
locale: FrontendLocaleData,
sensorNumericDeviceClasses: string[],
config: HassConfig,
entity: EntityRegistryDisplayEntry | undefined,
entityId: string,
@@ -90,7 +88,6 @@ export const computeStateDisplayFromEntityAttributes = (
const parts = computeStateToPartsFromEntityAttributes(
localize,
locale,
sensorNumericDeviceClasses,
config,
entity,
entityId,
@@ -103,7 +100,6 @@ export const computeStateDisplayFromEntityAttributes = (
const computeStateToPartsFromEntityAttributes = (
localize: LocalizeFunc,
locale: FrontendLocaleData,
sensorNumericDeviceClasses: string[],
config: HassConfig,
entity: EntityRegistryDisplayEntry | undefined,
entityId: string,
@@ -126,7 +122,7 @@ const computeStateToPartsFromEntityAttributes = (
if (
isNumericFromAttributes(
attributes,
domain === "sensor" ? sensorNumericDeviceClasses : []
domain === "sensor" ? SENSOR_NUMERIC_DEVICE_CLASSES : []
) ||
is_number_domain
) {
@@ -314,7 +310,6 @@ export const computeStateToParts = (
localize: LocalizeFunc,
stateObj: HassEntity,
locale: FrontendLocaleData,
sensorNumericDeviceClasses: string[],
config: HassConfig,
entities: HomeAssistant["entities"],
state?: string
@@ -325,7 +320,6 @@ export const computeStateToParts = (
return computeStateToPartsFromEntityAttributes(
localize,
locale,
sensorNumericDeviceClasses,
config,
entity,
stateObj.entity_id,
+3 -20
View File
@@ -46,8 +46,7 @@ export const computeFormatFunctions = async (
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
floors: HomeAssistant["floors"],
sensorNumericDeviceClasses: string[]
floors: HomeAssistant["floors"]
): Promise<{
formatEntityState: FormatEntityStateFunc;
formatEntityStateToParts: FormatEntityStateToPartsFunc;
@@ -66,25 +65,9 @@ export const computeFormatFunctions = async (
return {
formatEntityState: (stateObj, state) =>
computeStateDisplay(
localize,
stateObj,
locale,
sensorNumericDeviceClasses,
config,
entities,
state
),
computeStateDisplay(localize, stateObj, locale, config, entities, state),
formatEntityStateToParts: (stateObj, state) =>
computeStateToParts(
localize,
stateObj,
locale,
sensorNumericDeviceClasses,
config,
entities,
state
),
computeStateToParts(localize, stateObj, locale, config, entities, state),
formatEntityAttributeValue: (stateObj, attribute, value) =>
computeAttributeValueDisplay(
localize,
+2 -5
View File
@@ -10,6 +10,7 @@ import { computeStateDisplayFromEntityAttributes } from "../common/entity/comput
import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import { SENSOR_NUMERIC_DEVICE_CLASSES } from "./sensor_numeric_device_classes";
import type { FrontendLocaleData } from "./translation";
import type { Statistics } from "./recorder";
@@ -346,7 +347,6 @@ const processTimelineEntity = (
state_localize: computeStateDisplayFromEntityAttributes(
localize,
locale,
[], // numeric device classes not used for Timeline
config,
entities[entityId],
entityId,
@@ -470,7 +470,6 @@ export const convertStatisticsToHistory = (
hass: HomeAssistant,
statistics: Statistics,
statisticIds: string[],
sensorNumericDeviceClasses: string[],
splitDeviceClasses = false
): HistoryResult => {
// Maintain the statistic id ordering
@@ -498,7 +497,6 @@ export const convertStatisticsToHistory = (
statsHistoryStates,
[],
hass.localize,
sensorNumericDeviceClasses,
splitDeviceClasses,
true
);
@@ -528,7 +526,6 @@ export const computeHistory = (
stateHistory: HistoryStates,
entityIds: string[],
localize: LocalizeFunc,
sensorNumericalDeviceClasses: string[],
splitDeviceClasses = false,
forceNumeric = false
): HistoryResult => {
@@ -575,7 +572,7 @@ export const computeHistory = (
domain,
currentState,
numericStateFromHistory,
sensorNumericalDeviceClasses,
SENSOR_NUMERIC_DEVICE_CLASSES,
forceNumeric
);
+8 -5
View File
@@ -11,6 +11,7 @@ import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE, UNKNOWN } from "./entity/entity";
import { isNumericEntity } from "./history";
import { SENSOR_NUMERIC_DEVICE_CLASSES } from "./sensor_numeric_device_classes";
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
export const CONTINUOUS_DOMAINS = ["counter", "proximity"];
@@ -369,14 +370,16 @@ export const localizeStateMessage = (
});
};
export const filterLogbookCompatibleEntities = (
entity,
sensorNumericDeviceClasses: string[] = []
) => {
export const filterLogbookCompatibleEntities = (entity) => {
const domain = computeStateDomain(entity);
const continuous =
CONTINUOUS_DOMAINS.includes(domain) ||
(domain === "sensor" &&
isNumericEntity(domain, entity, undefined, sensorNumericDeviceClasses));
isNumericEntity(
domain,
entity,
undefined,
SENSOR_NUMERIC_DEVICE_CLASSES
));
return !continuous;
};
-25
View File
@@ -23,28 +23,3 @@ export const getSensorDeviceClassConvertibleUnits = (
type: "sensor/device_class_convertible_units",
device_class: deviceClass,
});
export interface SensorNumericDeviceClasses {
numeric_device_classes: string[];
}
let sensorNumericDeviceClassesCache:
| Promise<SensorNumericDeviceClasses>
| undefined;
export const getSensorNumericDeviceClasses = async (
hass: HomeAssistant
): Promise<SensorNumericDeviceClasses> => {
if (sensorNumericDeviceClassesCache) {
return sensorNumericDeviceClassesCache;
}
sensorNumericDeviceClassesCache = hass
.callWS<SensorNumericDeviceClasses>({
type: "sensor/numeric_device_classes",
})
.catch((err: Error) => {
sensorNumericDeviceClassesCache = undefined;
throw err;
});
return sensorNumericDeviceClassesCache!;
};
+63
View File
@@ -0,0 +1,63 @@
// This file is auto-generated from Home Assistant Core's `SensorDeviceClass`
// (all values minus `NON_NUMERIC_DEVICE_CLASSES`). Do not edit by hand.
// Regenerate with `script/gen_numeric_device_classes`.
export const SENSOR_NUMERIC_DEVICE_CLASSES: string[] = [
"absolute_humidity",
"apparent_power",
"aqi",
"area",
"atmospheric_pressure",
"battery",
"blood_glucose_concentration",
"carbon_dioxide",
"carbon_monoxide",
"conductivity",
"current",
"data_rate",
"data_size",
"distance",
"duration",
"energy",
"energy_distance",
"energy_storage",
"frequency",
"gas",
"humidity",
"illuminance",
"irradiance",
"moisture",
"monetary",
"nitrogen_dioxide",
"nitrogen_monoxide",
"nitrous_oxide",
"ozone",
"ph",
"pm1",
"pm10",
"pm25",
"pm4",
"power",
"power_factor",
"precipitation",
"precipitation_intensity",
"pressure",
"reactive_energy",
"reactive_power",
"signal_strength",
"sound_pressure",
"speed",
"sulphur_dioxide",
"temperature",
"temperature_delta",
"volatile_organic_compounds",
"volatile_organic_compounds_parts",
"voltage",
"volume",
"volume_flow_rate",
"volume_storage",
"water",
"weight",
"wind_direction",
"wind_speed",
];
+3 -3
View File
@@ -5,6 +5,7 @@ import type { GroupEntity } from "../../data/group";
import { computeGroupDomain } from "../../data/group";
import { isNumericEntity } from "../../data/history";
import { CONTINUOUS_DOMAINS } from "../../data/logbook";
import { SENSOR_NUMERIC_DEVICE_CLASSES } from "../../data/sensor_numeric_device_classes";
import type { HomeAssistant } from "../../types";
export const MORE_INFO_VIEWS = [
@@ -120,8 +121,7 @@ export const computeShowHistoryComponent = (
export const computeShowLogBookComponent = (
hass: HomeAssistant,
entityId: string,
sensorNumericalDeviceClasses: string[] = []
entityId: string
): boolean => {
if (!isComponentLoaded(hass.config, "logbook")) {
return false;
@@ -140,7 +140,7 @@ export const computeShowLogBookComponent = (
domain,
stateObj,
undefined,
sensorNumericalDeviceClasses
SENSOR_NUMERIC_DEVICE_CLASSES
)) ||
DOMAINS_MORE_INFO_NO_HISTORY.includes(domain)
) {
+1 -14
View File
@@ -62,7 +62,6 @@ import {
import { subscribeLabFeature } from "../../data/labs";
import type { ItemType } from "../../data/search";
import { SearchableDomains } from "../../data/search";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import { DirtyStateProviderMixin } from "../../mixins/dirty-state-provider-mixin";
import type { EntitySettingsState } from "../../panels/config/entities/entity-registry-settings-editor";
import type { Helper } from "../../panels/config/helpers/const";
@@ -164,8 +163,6 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
@state() private _isEscapeEnabled = true;
@state() private _sensorNumericDeviceClasses?: string[] = [];
@state() private _newTriggersAndConditions = false;
protected scrollFadeThreshold = 24;
@@ -257,11 +254,7 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
return (
DOMAINS_WITH_MORE_INFO.includes(domain) &&
(computeShowHistoryComponent(this.hass, this._entityId!) ||
computeShowLogBookComponent(
this.hass,
this._entityId!,
this._sensorNumericDeviceClasses
))
computeShowLogBookComponent(this.hass, this._entityId!))
);
}
@@ -537,11 +530,6 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
this._setView("related");
}
private async _loadNumericDeviceClasses() {
const deviceClasses = await getSensorNumericDeviceClasses(this.hass);
this._sensorNumericDeviceClasses = deviceClasses.numeric_device_classes;
}
protected render() {
if (!this._entityId) {
return nothing;
@@ -952,7 +940,6 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
super.firstUpdated(changedProps);
this.addEventListener("close-dialog", () => this.closeDialog());
this.addEventListener("close-child-view", () => this._goBack());
this._loadNumericDeviceClasses();
}
protected updated(changedProps: PropertyValues) {
@@ -1,6 +1,5 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import {
computeShowHistoryComponent,
@@ -8,7 +7,6 @@ import {
} from "./const";
import "./ha-more-info-history";
import "./ha-more-info-logbook";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
@customElement("ha-more-info-history-and-logbook")
export class MoreInfoHistoryAndLogbook extends LitElement {
@@ -16,18 +14,6 @@ export class MoreInfoHistoryAndLogbook extends LitElement {
@property({ attribute: false }) public entityId!: string;
@state() private _sensorNumericDeviceClasses?: string[] = [];
private async _loadNumericDeviceClasses() {
const deviceClasses = await getSensorNumericDeviceClasses(this.hass);
this._sensorNumericDeviceClasses = deviceClasses.numeric_device_classes;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
this._loadNumericDeviceClasses();
}
protected render() {
return html`
${computeShowHistoryComponent(this.hass, this.entityId)
@@ -38,11 +24,7 @@ export class MoreInfoHistoryAndLogbook extends LitElement {
></ha-more-info-history>
`
: ""}
${computeShowLogBookComponent(
this.hass,
this.entityId,
this._sensorNumericDeviceClasses
)
${computeShowLogBookComponent(this.hass, this.entityId)
? html`
<ha-more-info-logbook
.hass=${this.hass}
+1 -25
View File
@@ -19,7 +19,6 @@ import type {
StatisticsTypes,
} from "../../data/recorder";
import { fetchStatistics, getStatisticMetadata } from "../../data/recorder";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
@@ -246,28 +245,6 @@ export class MoreInfoHistory extends LitElement {
this._unsubscribeHistory();
}
// Mark as subscribing before the await to prevent re-entrant calls
const sentinel = Promise.resolve(undefined) as NonNullable<
typeof this._subscribed
>;
this._subscribed = sentinel;
let sensorNumericDeviceClasses: string[];
try {
({ numeric_device_classes: sensorNumericDeviceClasses } =
await getSensorNumericDeviceClasses(this.hass));
} catch (_err) {
if (this._subscribed === sentinel) {
this._subscribed = undefined;
}
return;
}
// Bail out if a newer call replaced our sentinel while we were awaiting
if (this._subscribed !== sentinel) {
return;
}
this._subscribed = subscribeHistoryStatesTimeWindow(
this.hass!,
(combinedHistory) => {
@@ -279,8 +256,7 @@ export class MoreInfoHistory extends LitElement {
this.hass!,
combinedHistory,
[this.entityId],
this.hass!.localize,
sensorNumericDeviceClasses
this.hass!.localize
);
},
24,
+2 -20
View File
@@ -1,10 +1,8 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { computeDomain } from "../../common/entity/compute_domain";
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import type { HomeAssistant } from "../../types";
import {
computeShowHistoryComponent,
@@ -30,18 +28,6 @@ export class MoreInfoInfo extends LitElement {
@property({ attribute: false }) public data?: Record<string, any>;
@state() private _sensorNumericDeviceClasses?: string[] = [];
private async _loadNumericDeviceClasses() {
const deviceClasses = await getSensorNumericDeviceClasses(this.hass);
this._sensorNumericDeviceClasses = deviceClasses.numeric_device_classes;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
this._loadNumericDeviceClasses();
}
protected render() {
const entityId = this.entityId;
const stateObj = this.hass.states[entityId] as HassEntity | undefined;
@@ -92,11 +78,7 @@ export class MoreInfoInfo extends LitElement {
.entityId=${this.entityId}
></ha-more-info-history>`}
${DOMAINS_WITH_MORE_INFO.includes(domain) ||
!computeShowLogBookComponent(
this.hass,
entityId,
this._sensorNumericDeviceClasses
)
!computeShowLogBookComponent(this.hass, entityId)
? ""
: html`<ha-more-info-logbook
.hass=${this.hass}
+1 -2
View File
@@ -155,8 +155,7 @@ export const provideHass = (
hass().entities,
hass().devices,
hass().areas,
hass().floors,
[] // numericDeviceClasses
hass().floors
);
hass().updateHass({
formatEntityState,
@@ -83,10 +83,8 @@ import {
createOptionsFlow,
handleOptionsFlowStep,
} from "../../../data/options_flow";
import {
getSensorDeviceClassConvertibleUnits,
getSensorNumericDeviceClasses,
} from "../../../data/sensor";
import { getSensorDeviceClassConvertibleUnits } from "../../../data/sensor";
import { SENSOR_NUMERIC_DEVICE_CLASSES } from "../../../data/sensor_numeric_device_classes";
import { VacuumEntityFeature } from "../../../data/vacuum";
import type { WeatherUnits } from "../../../data/weather";
import { getWeatherConvertibleUnits } from "../../../data/weather";
@@ -238,8 +236,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
@state() private _sensorDeviceClassConvertibleUnits?: string[];
@state() private _sensorNumericalDeviceClasses?: string[];
@state() private _weatherConvertibleUnits?: WeatherUnits;
@state() private _defaultCode?: EntitySettingsState["defaultCode"];
@@ -423,14 +419,6 @@ 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,
@@ -791,7 +779,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 &&
this._sensorNumericalDeviceClasses?.includes(this._deviceClass)) ||
SENSOR_NUMERIC_DEVICE_CLASSES.includes(this._deviceClass)) ||
stateObj?.attributes.unit_of_measurement ||
stateObj?.attributes.state_class)
? html`
-34
View File
@@ -46,7 +46,6 @@ import {
} from "../../data/history";
import { fetchStatistics } from "../../data/recorder";
import { resolveEntityIDs } from "../../data/selector";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { haStyle, haStyleScrollbar } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
@@ -296,19 +295,10 @@ class HaPanelHistory extends LitElement {
return;
}
let sensorNumericDeviceClasses: string[];
try {
({ numeric_device_classes: sensorNumericDeviceClasses } =
await getSensorNumericDeviceClasses(this.hass));
} catch (_err) {
return;
}
this._statisticsHistory = convertStatisticsToHistory(
this.hass!,
statistics,
statisticIds,
sensorNumericDeviceClasses,
true
);
}
@@ -329,29 +319,6 @@ class HaPanelHistory extends LitElement {
const now = new Date();
// Mark as subscribing before the await to prevent re-entrant calls
const sentinel = Promise.resolve(undefined) as NonNullable<
typeof this._subscribed
>;
this._subscribed = sentinel;
let sensorNumericDeviceClasses: string[];
try {
({ numeric_device_classes: sensorNumericDeviceClasses } =
await getSensorNumericDeviceClasses(this.hass));
} catch (_err) {
if (this._subscribed === sentinel) {
this._subscribed = undefined;
this._isLoading = false;
}
return;
}
// Bail out if a newer call replaced our sentinel while we were awaiting
if (this._subscribed !== sentinel) {
return;
}
this._subscribed = subscribeHistory(
this.hass,
(history) => {
@@ -361,7 +328,6 @@ class HaPanelHistory extends LitElement {
history,
entityIds,
this.hass.localize,
sensorNumericDeviceClasses,
true
);
},
+1 -10
View File
@@ -23,7 +23,6 @@ import "../../components/ha-top-app-bar-fixed";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
import { filterLogbookCompatibleEntities } from "../../data/logbook";
import { resolveEntityIDs } from "../../data/selector";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "./ha-logbook";
@@ -49,8 +48,6 @@ export class HaPanelLogbook extends LitElement {
})
private _targetPickerValue: HassServiceTarget = {};
@state() private _sensorNumericDeviceClasses?: string[] = [];
public constructor() {
super();
@@ -108,7 +105,7 @@ export class HaPanelLogbook extends LitElement {
}
private _filterFunc: HaEntityPickerEntityFilterFunc = (entity) =>
filterLogbookCompatibleEntities(entity, this._sensorNumericDeviceClasses);
filterLogbookCompatibleEntities(entity);
protected willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
@@ -120,15 +117,9 @@ export class HaPanelLogbook extends LitElement {
this._applyURLParams();
}
private async _loadNumericDeviceClasses() {
const deviceClasses = await getSensorNumericDeviceClasses(this.hass);
this._sensorNumericDeviceClasses = deviceClasses.numeric_device_classes;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
this.hass.loadBackendTranslation("title");
this._loadNumericDeviceClasses();
const searchParams = extractSearchParamsObject();
if (searchParams.back === "1" && history.length > 1) {
@@ -18,7 +18,6 @@ import {
type HistoryResult,
} from "../../../data/history";
import { fetchStatistics } from "../../../data/recorder";
import { getSensorNumericDeviceClasses } from "../../../data/sensor";
import type { HomeAssistant } from "../../../types";
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
import { processConfigEntities } from "../common/process-config-entities";
@@ -150,30 +149,6 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
return;
}
// Mark as subscribing before the first await to prevent re-entrant calls
const sentinel = Promise.resolve(undefined) as NonNullable<
typeof this._subscribed
>;
this._subscribed = sentinel;
let sensorNumericDeviceClasses: string[];
try {
({ numeric_device_classes: sensorNumericDeviceClasses } =
await getSensorNumericDeviceClasses(this.hass!));
} catch (_err) {
if (this._subscribed === sentinel) {
this._subscribed = undefined;
}
return;
}
if (!this.isConnected || this._subscribed !== sentinel) {
if (this._subscribed === sentinel) {
this._subscribed = undefined;
}
return;
}
this._subscribed = subscribeHistoryStatesTimeWindow(
this.hass!,
(combinedHistory) => {
@@ -187,7 +162,6 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
combinedHistory,
this._entityIds,
this.hass!.localize,
sensorNumericDeviceClasses,
this._config?.split_device_classes
);
@@ -201,7 +175,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
return undefined;
});
await this._fetchStatistics(sensorNumericDeviceClasses);
await this._fetchStatistics();
this._setRedrawTimer();
}
@@ -216,7 +190,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
}
}
private async _fetchStatistics(sensorNumericDeviceClasses: string[]) {
private async _fetchStatistics() {
if (this._hoursToShow < 1) {
// Statistics are hourly aggregates, not useful for sub-hour windows
return;
@@ -239,7 +213,6 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
this.hass!,
statistics,
this._entityIds,
sensorNumericDeviceClasses,
this._config?.split_device_classes
);
@@ -26,7 +26,7 @@ import type {
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { SelectOption } from "../../../../data/selector";
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
import { SENSOR_NUMERIC_DEVICE_CLASSES } from "../../../../data/sensor_numeric_device_classes";
import type { HomeAssistant } from "../../../../types";
import type {
LovelaceCardFeatureConfig,
@@ -77,8 +77,6 @@ export class HuiAreaCardEditor
@state() private _config?: AreaCardConfig;
@state() private _numericDeviceClasses?: string[];
@state() private _featureContext: AreaCardFeatureContext = {};
private _schema = memoizeOne(
@@ -344,14 +342,6 @@ export class HuiAreaCardEditor
};
}
protected async updated() {
if (this.hass && !this._numericDeviceClasses) {
const { numeric_device_classes: sensorNumericDeviceClasses } =
await getSensorNumericDeviceClasses(this.hass);
this._numericDeviceClasses = sensorNumericDeviceClasses;
}
}
private _featuresSchema = memoizeOne(
(localize: LocalizeFunc, vertical: boolean) =>
[
@@ -399,7 +389,7 @@ export class HuiAreaCardEditor
const possibleSensorClasses = this._sensorClassesForArea(
this._config.area,
this._config.exclude_entities,
this._numericDeviceClasses
SENSOR_NUMERIC_DEVICE_CLASSES
);
const binarySelectOptions = this._buildBinaryOptions(
possibleBinaryClasses,
@@ -20,7 +20,6 @@ import type { HaEntityPickerEntityFilterFunc } from "../../../../data/entity/ent
import { filterLogbookCompatibleEntities } from "../../../../data/logbook";
import { targetStruct } from "../../../../data/script";
import { resolveEntityIDs } from "../../../../data/selector";
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
import type { HomeAssistant } from "../../../../types";
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-logbook-card";
import type { LogbookCardConfig } from "../../cards/types";
@@ -71,8 +70,6 @@ export class HuiLogbookCardEditor
@state() private _config?: LogbookCardConfig;
@state() private _sensorNumericDeviceClasses?: string[];
public setConfig(config: LogbookCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
@@ -94,20 +91,6 @@ export class HuiLogbookCardEditor
);
}
private async _loadNumericDeviceClasses(hass: HomeAssistant) {
// ensures that the _load function is not called a second time
// if another updated occurs before the async function returns
this._sensorNumericDeviceClasses = [];
const deviceClasses = await getSensorNumericDeviceClasses(hass);
this._sensorNumericDeviceClasses = deviceClasses.numeric_device_classes;
}
protected updated() {
if (this.hass && !this._sensorNumericDeviceClasses) {
this._loadNumericDeviceClasses(this.hass);
}
}
protected render() {
if (!this.hass || !this._config) {
return nothing;
@@ -157,7 +140,7 @@ export class HuiLogbookCardEditor
);
private _filterFunc: HaEntityPickerEntityFilterFunc = (entity) =>
filterLogbookCompatibleEntities(entity, this._sensorNumericDeviceClasses);
filterLogbookCompatibleEntities(entity);
private _entitiesChanged(ev: CustomEvent): void {
this._config = { ...this._config!, target: ev.detail.value };
+4 -33
View File
@@ -1,7 +1,5 @@
import type { PropertyValues } from "lit";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { computeFormatFunctions } from "../common/translations/entity-state";
import { getSensorNumericDeviceClasses } from "../data/sensor";
import type { Constructor, HomeAssistant } from "../types";
import type { HassBaseEl } from "./hass-base-mixin";
@@ -36,47 +34,20 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) => {
}
private _updateFormatFunctions = async () => {
if (!this.hass || !this.hass.config) {
if (!this.hass?.config) {
return;
}
let sensorNumericDeviceClasses: string[] = [];
if (isComponentLoaded(this.hass.config, "sensor")) {
try {
sensorNumericDeviceClasses = (
await getSensorNumericDeviceClasses(this.hass)
).numeric_device_classes;
} catch (_err: any) {
// ignore
}
}
const {
formatEntityState,
formatEntityStateToParts,
formatEntityAttributeName,
formatEntityAttributeValue,
formatEntityAttributeValueToParts,
formatEntityName,
} = await computeFormatFunctions(
const formatFunctions = await computeFormatFunctions(
this.hass.localize,
this.hass.locale,
this.hass.config,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors,
sensorNumericDeviceClasses
this.hass.floors
);
this._updateHass({
formatEntityState,
formatEntityStateToParts,
formatEntityAttributeName,
formatEntityAttributeValue,
formatEntityAttributeValueToParts,
formatEntityName,
});
this._updateHass(formatFunctions);
};
}
return StateDisplayMixin;
+18 -157
View File
@@ -23,8 +23,6 @@ describe("computeStateDisplay", () => {
const localize = (message, ...args) =>
message + (args.length ? ": " + args.join(",") : "");
const numericDeviceClasses = [];
beforeEach(() => {
localeData = {
language: "en",
@@ -43,14 +41,7 @@ describe("computeStateDisplay", () => {
attributes: {},
};
assert.strictEqual(
computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}),
"component.binary_sensor.entity_component._.state.off"
);
});
@@ -64,14 +55,7 @@ describe("computeStateDisplay", () => {
},
};
assert.strictEqual(
computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}),
"component.binary_sensor.entity_component.moisture.state.off"
);
});
@@ -91,14 +75,7 @@ describe("computeStateDisplay", () => {
},
};
assert.strictEqual(
computeStateDisplay(
altLocalize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}),
"component.binary_sensor.entity_component.invalid_device_class.state.off"
);
});
@@ -112,14 +89,7 @@ describe("computeStateDisplay", () => {
},
};
assert.strictEqual(
computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}),
"123 m"
);
});
@@ -139,14 +109,7 @@ describe("computeStateDisplay", () => {
},
};
assert.strictEqual(
computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
entities
),
computeStateDisplay(localize, stateObj, localeData, demoConfig, entities),
"1,234 component.custom_integration.entity.sensor.custom_translation.unit_of_measurement"
);
});
@@ -160,14 +123,7 @@ describe("computeStateDisplay", () => {
},
};
assert.strictEqual(
computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}),
"1,234.5 m"
);
});
@@ -181,14 +137,7 @@ describe("computeStateDisplay", () => {
},
};
assert.strictEqual(
computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}),
"1,234.5"
);
});
@@ -208,14 +157,7 @@ describe("computeStateDisplay", () => {
},
};
assert.strictEqual(
computeStateDisplay(
altLocalize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}),
"state.default.unknown"
);
});
@@ -235,14 +177,7 @@ describe("computeStateDisplay", () => {
},
};
assert.strictEqual(
computeStateDisplay(
altLocalize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}),
"state.default.unavailable"
);
});
@@ -262,14 +197,7 @@ describe("computeStateDisplay", () => {
attributes: {},
};
assert.strictEqual(
computeStateDisplay(
altLocalize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}),
"component.sensor.entity_component._.state.custom_state"
);
});
@@ -293,7 +221,6 @@ describe("computeStateDisplay", () => {
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
entities
),
@@ -328,28 +255,14 @@ describe("computeStateDisplay", () => {
};
it("Uses am/pm time format", () => {
assert.strictEqual(
computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}),
"November 18, 2017 at 11:12 PM"
);
});
it("Uses 24h time format", () => {
localeData.time_format = TimeFormat.twenty_four;
assert.strictEqual(
computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}),
"November 18, 2017 at 23:12"
);
});
@@ -371,14 +284,7 @@ describe("computeStateDisplay", () => {
},
};
assert.strictEqual(
computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}),
"November 18, 2017"
);
});
@@ -401,28 +307,14 @@ describe("computeStateDisplay", () => {
it("Uses am/pm time format", () => {
localeData.time_format = TimeFormat.am_pm;
assert.strictEqual(
computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}),
"11:12 PM"
);
});
it("Uses 24h time format", () => {
localeData.time_format = TimeFormat.twenty_four;
assert.strictEqual(
computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(localize, stateObj, localeData, demoConfig, {}),
"23:12"
);
});
@@ -449,7 +341,6 @@ describe("computeStateDisplay", () => {
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{},
"2021-07-04 15:40:03"
@@ -464,7 +355,6 @@ describe("computeStateDisplay", () => {
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{},
"2021-07-04 15:40:03"
@@ -494,7 +384,6 @@ describe("computeStateDisplay", () => {
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{},
"2021-07-04"
@@ -525,7 +414,6 @@ describe("computeStateDisplay", () => {
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{},
"17:05:07"
@@ -540,7 +428,6 @@ describe("computeStateDisplay", () => {
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{},
"17:05:07"
@@ -563,14 +450,7 @@ describe("computeStateDisplay", () => {
attributes: {},
};
assert.strictEqual(
computeStateDisplay(
altLocalize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}),
"state.default.unavailable"
);
});
@@ -585,14 +465,7 @@ describe("computeStateDisplay", () => {
attributes: {},
};
assert.strictEqual(
computeStateDisplay(
altLocalize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
{}
),
computeStateDisplay(altLocalize, stateObj, localeData, demoConfig, {}),
"My Custom State"
);
});
@@ -610,14 +483,7 @@ describe("computeStateDisplay", () => {
},
};
assert.strictEqual(
computeStateDisplay(
localize,
stateObj,
localeData,
numericDeviceClasses,
demoConfig,
entities
),
computeStateDisplay(localize, stateObj, localeData, demoConfig, entities),
"component.custom_integration.entity.sensor.custom_translation.state.custom_state"
);
});
@@ -631,7 +497,6 @@ describe("computeStateDisplayFromEntityAttributes with numeric device classes",
{
language: "en",
} as FrontendLocaleData,
[],
{} as HassConfig,
{
display_precision: 2,
@@ -652,7 +517,6 @@ describe("computeStateDisplayFromEntityAttributes with numeric device classes",
{
language: "en",
} as FrontendLocaleData,
[],
{} as HassConfig,
undefined,
"number.test",
@@ -672,7 +536,6 @@ describe("computeStateDisplayFromEntityAttributes with numeric device classes",
{
language: "en",
} as FrontendLocaleData,
[],
{} as HassConfig,
undefined,
"number.test",
@@ -692,7 +555,6 @@ describe("computeStateDisplayFromEntityAttributes with numeric device classes",
{
language: "en",
} as FrontendLocaleData,
[],
{} as HassConfig,
undefined,
"number.test",
@@ -720,7 +582,6 @@ describe("computeStateDisplayFromEntityAttributes datetime device calss", () =>
{
language: "en",
} as FrontendLocaleData,
[],
{} as HassConfig,
undefined,
"button.test",