diff --git a/src/panels/lovelace/cards/hui-area-card.ts b/src/panels/lovelace/cards/hui-area-card.ts
index 7fe1749099..eff790b5fe 100644
--- a/src/panels/lovelace/cards/hui-area-card.ts
+++ b/src/panels/lovelace/cards/hui-area-card.ts
@@ -3,11 +3,9 @@ import {
mdiLightbulbMultiple,
mdiLightbulbMultipleOff,
mdiRun,
- mdiThermometer,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiWaterAlert,
- mdiWaterPercent,
} from "@mdi/js";
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
@@ -23,13 +21,16 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
-import { STATES_OFF } from "../../../common/const";
+import { STATES_OFF, FIXED_DEVICE_CLASS_ICONS } from "../../../common/const";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeDomain } from "../../../common/entity/compute_domain";
import { binarySensorIcon } from "../../../common/entity/binary_sensor_icon";
import { domainIcon } from "../../../common/entity/domain_icon";
import { navigate } from "../../../common/navigate";
-import { formatNumber } from "../../../common/number/format_number";
+import {
+ formatNumber,
+ isNumericState,
+} from "../../../common/number/format_number";
import { subscribeOne } from "../../../common/util/subscribe-one";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
import "../../../components/entity/state-badge";
@@ -57,6 +58,7 @@ import "../components/hui-image";
import "../components/hui-warning";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { AreaCardConfig } from "./types";
+import { blankBeforeUnit } from "../../../common/translations/blank_before_unit";
export const DEFAULT_ASPECT_RATIO = "16:9";
@@ -77,10 +79,6 @@ const DOMAIN_ICONS = {
light: { on: mdiLightbulbMultiple, off: mdiLightbulbMultipleOff },
switch: { on: mdiToggleSwitch, off: mdiToggleSwitchOff },
fan: { on: domainIcon("fan"), off: domainIcon("fan") },
- sensor: {
- temperature: mdiThermometer,
- humidity: mdiWaterPercent,
- },
binary_sensor: {
motion: mdiRun,
moisture: mdiWaterAlert,
@@ -215,10 +213,7 @@ export class HuiAreaCard
}
let uom;
const values = entities.filter((entity) => {
- if (
- !entity.attributes.unit_of_measurement ||
- isNaN(Number(entity.state))
- ) {
+ if (!isNumericState(entity) || isNaN(Number(entity.state))) {
return false;
}
if (!uom) {
@@ -236,7 +231,7 @@ export class HuiAreaCard
);
return `${formatNumber(sum / values.length, this.hass!.locale, {
maximumFractionDigits: 1,
- })} ${uom}`;
+ })}${uom ? blankBeforeUnit(uom, this.hass!.locale) : ""}${uom || ""}`;
}
private _area = memoizeOne(
@@ -281,6 +276,9 @@ export class HuiAreaCard
this._config = config;
this._deviceClasses = { ...DEVICE_CLASSES };
+ if (config.sensor_classes) {
+ this._deviceClasses.sensor = config.sensor_classes;
+ }
if (config.alert_classes) {
this._deviceClasses.binary_sensor = config.alert_classes;
}
@@ -385,20 +383,19 @@ export class HuiAreaCard
if (!(domain in entitiesByDomain)) {
return;
}
- DEVICE_CLASSES[domain].forEach((deviceClass) => {
+ this._deviceClasses[domain].forEach((deviceClass) => {
if (
entitiesByDomain[domain].some(
(entity) => entity.attributes.device_class === deviceClass
)
) {
- sensors.push(html`
- ${DOMAIN_ICONS[domain][deviceClass]
- ? html``
- : ""}
- ${this._average(domain, deviceClass)}
- `);
+ const icon = FIXED_DEVICE_CLASS_ICONS[deviceClass];
+ sensors.push(
+ html`
+ ${icon ? html`` : ""}
+ ${this._average(domain, deviceClass)}
+
`
+ );
}
});
});
@@ -566,6 +563,12 @@ export class HuiAreaCard
margin-top: 8px;
}
+ .sensor {
+ white-space: nowrap;
+ float: left;
+ margin-right: 4px;
+ }
+
.alerts {
padding: 16px;
}
diff --git a/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts
index c49e9bf99e..f46f279b45 100644
--- a/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts
@@ -24,6 +24,7 @@ import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { caseInsensitiveStringCompare } from "../../../../common/string/compare";
import { SelectOption } from "../../../../data/selector";
+import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -35,6 +36,7 @@ const cardConfigStruct = assign(
camera_view: optional(string()),
aspect_ratio: optional(string()),
alert_classes: optional(array(string())),
+ sensor_classes: optional(array(string())),
})
);
@@ -47,8 +49,14 @@ export class HuiAreaCardEditor
@state() private _config?: AreaCardConfig;
+ @state() private _numericDeviceClasses?: string[];
+
private _schema = memoizeOne(
- (showCamera: boolean, binaryClasses: SelectOption[]) =>
+ (
+ showCamera: boolean,
+ binaryClasses: SelectOption[],
+ sensorClasses: SelectOption[]
+ ) =>
[
{ name: "area", selector: { area: {} } },
{ name: "show_camera", required: false, selector: { boolean: {} } },
@@ -88,13 +96,37 @@ export class HuiAreaCardEditor
},
},
},
+ {
+ name: "sensor_classes",
+ selector: {
+ select: {
+ reorder: true,
+ multiple: true,
+ custom_value: true,
+ options: sensorClasses,
+ },
+ },
+ },
] as const
);
- private _binaryClassesForArea = memoizeOne((area: string): string[] => {
+ private _binaryClassesForArea = memoizeOne((area: string): string[] =>
+ this._classesForArea(area, "binary_sensor")
+ );
+
+ private _sensorClassesForArea = memoizeOne(
+ (area: string, numericDeviceClasses?: string[]): string[] =>
+ this._classesForArea(area, "sensor", numericDeviceClasses)
+ );
+
+ private _classesForArea(
+ area: string,
+ domain: "sensor" | "binary_sensor",
+ numericDeviceClasses?: string[] | undefined
+ ): string[] {
const entities = Object.values(this.hass!.entities).filter(
(e) =>
- computeDomain(e.entity_id) === "binary_sensor" &&
+ computeDomain(e.entity_id) === domain &&
!e.entity_category &&
!e.hidden &&
(e.area_id === area ||
@@ -103,58 +135,92 @@ export class HuiAreaCardEditor
const classes = entities
.map((e) => this.hass!.states[e.entity_id]?.attributes.device_class || "")
- .filter((c) => c);
+ .filter(
+ (c) =>
+ c &&
+ (domain !== "sensor" ||
+ !numericDeviceClasses ||
+ numericDeviceClasses.includes(c))
+ );
return [...new Set(classes)];
- });
+ }
- private _buildOptions = memoizeOne(
- (possibleClasses: string[], currentClasses: string[]): SelectOption[] => {
- const options = [...new Set([...possibleClasses, ...currentClasses])].map(
- (deviceClass) => ({
- value: deviceClass,
- label:
- this.hass!.localize(
- `component.binary_sensor.entity_component.${deviceClass}.name`
- ) || deviceClass,
- })
- );
- options.sort((a, b) =>
- caseInsensitiveStringCompare(
- a.label,
- b.label,
- this.hass!.locale.language
- )
- );
-
- return options;
- }
+ private _buildBinaryOptions = memoizeOne(
+ (possibleClasses: string[], currentClasses: string[]): SelectOption[] =>
+ this._buildOptions("binary_sensor", possibleClasses, currentClasses)
);
+ private _buildSensorOptions = memoizeOne(
+ (possibleClasses: string[], currentClasses: string[]): SelectOption[] =>
+ this._buildOptions("sensor", possibleClasses, currentClasses)
+ );
+
+ private _buildOptions(
+ domain: "sensor" | "binary_sensor",
+ possibleClasses: string[],
+ currentClasses: string[]
+ ): SelectOption[] {
+ const options = [...new Set([...possibleClasses, ...currentClasses])].map(
+ (deviceClass) => ({
+ value: deviceClass,
+ label:
+ this.hass!.localize(
+ `component.${domain}.entity_component.${deviceClass}.name`
+ ) || deviceClass,
+ })
+ );
+ options.sort((a, b) =>
+ caseInsensitiveStringCompare(a.label, b.label, this.hass!.locale.language)
+ );
+
+ return options;
+ }
+
public setConfig(config: AreaCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
}
+ protected async updated() {
+ if (this.hass && !this._numericDeviceClasses) {
+ const { numeric_device_classes: sensorNumericDeviceClasses } =
+ await getSensorNumericDeviceClasses(this.hass);
+ this._numericDeviceClasses = sensorNumericDeviceClasses;
+ }
+ }
+
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
- const possibleClasses = this._binaryClassesForArea(this._config.area || "");
- const selectOptions = this._buildOptions(
- possibleClasses,
+ const possibleBinaryClasses = this._binaryClassesForArea(
+ this._config.area || ""
+ );
+ const possibleSensorClasses = this._sensorClassesForArea(
+ this._config.area || "",
+ this._numericDeviceClasses
+ );
+ const binarySelectOptions = this._buildBinaryOptions(
+ possibleBinaryClasses,
this._config.alert_classes || DEVICE_CLASSES.binary_sensor
);
+ const sensorSelectOptions = this._buildSensorOptions(
+ possibleSensorClasses,
+ this._config.sensor_classes || DEVICE_CLASSES.sensor
+ );
const schema = this._schema(
this._config.show_camera || false,
- selectOptions
+ binarySelectOptions,
+ sensorSelectOptions
);
const data = {
camera_view: "auto",
alert_classes: DEVICE_CLASSES.binary_sensor,
+ sensor_classes: DEVICE_CLASSES.sensor,
...this._config,
};
@@ -201,10 +267,6 @@ export class HuiAreaCardEditor
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.camera_view"
);
- case "alert_classes":
- return this.hass!.localize(
- "ui.panel.lovelace.editor.card.area.alert_classes"
- );
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.area.${schema.name}`
diff --git a/src/translations/en.json b/src/translations/en.json
index c7254e73c9..df0409ca76 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -5133,6 +5133,7 @@
"area": {
"name": "Area",
"alert_classes": "Alert Classes",
+ "sensor_classes": "Sensor Classes",
"description": "The Area card automatically displays entities of a specific area.",
"show_camera": "Show camera feed instead of area picture"
},