mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-26 18:56:39 +00:00
Custom device_classes for Area Card sensors (#19421)
* Custom device_classes for Area Card sensors * no sensor DOMAIN_ICONS
This commit is contained in:
parent
58c4bf081b
commit
7d4284d409
@ -3,11 +3,9 @@ import {
|
|||||||
mdiLightbulbMultiple,
|
mdiLightbulbMultiple,
|
||||||
mdiLightbulbMultipleOff,
|
mdiLightbulbMultipleOff,
|
||||||
mdiRun,
|
mdiRun,
|
||||||
mdiThermometer,
|
|
||||||
mdiToggleSwitch,
|
mdiToggleSwitch,
|
||||||
mdiToggleSwitchOff,
|
mdiToggleSwitchOff,
|
||||||
mdiWaterAlert,
|
mdiWaterAlert,
|
||||||
mdiWaterPercent,
|
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import {
|
import {
|
||||||
@ -23,13 +21,16 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import memoizeOne from "memoize-one";
|
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 { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
import { binarySensorIcon } from "../../../common/entity/binary_sensor_icon";
|
import { binarySensorIcon } from "../../../common/entity/binary_sensor_icon";
|
||||||
import { domainIcon } from "../../../common/entity/domain_icon";
|
import { domainIcon } from "../../../common/entity/domain_icon";
|
||||||
import { navigate } from "../../../common/navigate";
|
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 { subscribeOne } from "../../../common/util/subscribe-one";
|
||||||
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
||||||
import "../../../components/entity/state-badge";
|
import "../../../components/entity/state-badge";
|
||||||
@ -57,6 +58,7 @@ import "../components/hui-image";
|
|||||||
import "../components/hui-warning";
|
import "../components/hui-warning";
|
||||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||||
import { AreaCardConfig } from "./types";
|
import { AreaCardConfig } from "./types";
|
||||||
|
import { blankBeforeUnit } from "../../../common/translations/blank_before_unit";
|
||||||
|
|
||||||
export const DEFAULT_ASPECT_RATIO = "16:9";
|
export const DEFAULT_ASPECT_RATIO = "16:9";
|
||||||
|
|
||||||
@ -77,10 +79,6 @@ const DOMAIN_ICONS = {
|
|||||||
light: { on: mdiLightbulbMultiple, off: mdiLightbulbMultipleOff },
|
light: { on: mdiLightbulbMultiple, off: mdiLightbulbMultipleOff },
|
||||||
switch: { on: mdiToggleSwitch, off: mdiToggleSwitchOff },
|
switch: { on: mdiToggleSwitch, off: mdiToggleSwitchOff },
|
||||||
fan: { on: domainIcon("fan"), off: domainIcon("fan") },
|
fan: { on: domainIcon("fan"), off: domainIcon("fan") },
|
||||||
sensor: {
|
|
||||||
temperature: mdiThermometer,
|
|
||||||
humidity: mdiWaterPercent,
|
|
||||||
},
|
|
||||||
binary_sensor: {
|
binary_sensor: {
|
||||||
motion: mdiRun,
|
motion: mdiRun,
|
||||||
moisture: mdiWaterAlert,
|
moisture: mdiWaterAlert,
|
||||||
@ -215,10 +213,7 @@ export class HuiAreaCard
|
|||||||
}
|
}
|
||||||
let uom;
|
let uom;
|
||||||
const values = entities.filter((entity) => {
|
const values = entities.filter((entity) => {
|
||||||
if (
|
if (!isNumericState(entity) || isNaN(Number(entity.state))) {
|
||||||
!entity.attributes.unit_of_measurement ||
|
|
||||||
isNaN(Number(entity.state))
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!uom) {
|
if (!uom) {
|
||||||
@ -236,7 +231,7 @@ export class HuiAreaCard
|
|||||||
);
|
);
|
||||||
return `${formatNumber(sum / values.length, this.hass!.locale, {
|
return `${formatNumber(sum / values.length, this.hass!.locale, {
|
||||||
maximumFractionDigits: 1,
|
maximumFractionDigits: 1,
|
||||||
})} ${uom}`;
|
})}${uom ? blankBeforeUnit(uom, this.hass!.locale) : ""}${uom || ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _area = memoizeOne(
|
private _area = memoizeOne(
|
||||||
@ -281,6 +276,9 @@ export class HuiAreaCard
|
|||||||
this._config = config;
|
this._config = config;
|
||||||
|
|
||||||
this._deviceClasses = { ...DEVICE_CLASSES };
|
this._deviceClasses = { ...DEVICE_CLASSES };
|
||||||
|
if (config.sensor_classes) {
|
||||||
|
this._deviceClasses.sensor = config.sensor_classes;
|
||||||
|
}
|
||||||
if (config.alert_classes) {
|
if (config.alert_classes) {
|
||||||
this._deviceClasses.binary_sensor = config.alert_classes;
|
this._deviceClasses.binary_sensor = config.alert_classes;
|
||||||
}
|
}
|
||||||
@ -385,20 +383,19 @@ export class HuiAreaCard
|
|||||||
if (!(domain in entitiesByDomain)) {
|
if (!(domain in entitiesByDomain)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DEVICE_CLASSES[domain].forEach((deviceClass) => {
|
this._deviceClasses[domain].forEach((deviceClass) => {
|
||||||
if (
|
if (
|
||||||
entitiesByDomain[domain].some(
|
entitiesByDomain[domain].some(
|
||||||
(entity) => entity.attributes.device_class === deviceClass
|
(entity) => entity.attributes.device_class === deviceClass
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
sensors.push(html`
|
const icon = FIXED_DEVICE_CLASS_ICONS[deviceClass];
|
||||||
${DOMAIN_ICONS[domain][deviceClass]
|
sensors.push(
|
||||||
? html`<ha-svg-icon
|
html`<div class="sensor">
|
||||||
.path=${DOMAIN_ICONS[domain][deviceClass]}
|
${icon ? html`<ha-svg-icon .path=${icon}></ha-svg-icon>` : ""}
|
||||||
></ha-svg-icon>`
|
|
||||||
: ""}
|
|
||||||
${this._average(domain, deviceClass)}
|
${this._average(domain, deviceClass)}
|
||||||
`);
|
</div> `
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -566,6 +563,12 @@ export class HuiAreaCard
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sensor {
|
||||||
|
white-space: nowrap;
|
||||||
|
float: left;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.alerts {
|
.alerts {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
|||||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||||
import { caseInsensitiveStringCompare } from "../../../../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../../../../common/string/compare";
|
||||||
import { SelectOption } from "../../../../data/selector";
|
import { SelectOption } from "../../../../data/selector";
|
||||||
|
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
|
||||||
|
|
||||||
const cardConfigStruct = assign(
|
const cardConfigStruct = assign(
|
||||||
baseLovelaceCardConfig,
|
baseLovelaceCardConfig,
|
||||||
@ -35,6 +36,7 @@ const cardConfigStruct = assign(
|
|||||||
camera_view: optional(string()),
|
camera_view: optional(string()),
|
||||||
aspect_ratio: optional(string()),
|
aspect_ratio: optional(string()),
|
||||||
alert_classes: optional(array(string())),
|
alert_classes: optional(array(string())),
|
||||||
|
sensor_classes: optional(array(string())),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -47,8 +49,14 @@ export class HuiAreaCardEditor
|
|||||||
|
|
||||||
@state() private _config?: AreaCardConfig;
|
@state() private _config?: AreaCardConfig;
|
||||||
|
|
||||||
|
@state() private _numericDeviceClasses?: string[];
|
||||||
|
|
||||||
private _schema = memoizeOne(
|
private _schema = memoizeOne(
|
||||||
(showCamera: boolean, binaryClasses: SelectOption[]) =>
|
(
|
||||||
|
showCamera: boolean,
|
||||||
|
binaryClasses: SelectOption[],
|
||||||
|
sensorClasses: SelectOption[]
|
||||||
|
) =>
|
||||||
[
|
[
|
||||||
{ name: "area", selector: { area: {} } },
|
{ name: "area", selector: { area: {} } },
|
||||||
{ name: "show_camera", required: false, selector: { boolean: {} } },
|
{ 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
|
] 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(
|
const entities = Object.values(this.hass!.entities).filter(
|
||||||
(e) =>
|
(e) =>
|
||||||
computeDomain(e.entity_id) === "binary_sensor" &&
|
computeDomain(e.entity_id) === domain &&
|
||||||
!e.entity_category &&
|
!e.entity_category &&
|
||||||
!e.hidden &&
|
!e.hidden &&
|
||||||
(e.area_id === area ||
|
(e.area_id === area ||
|
||||||
@ -103,58 +135,92 @@ export class HuiAreaCardEditor
|
|||||||
|
|
||||||
const classes = entities
|
const classes = entities
|
||||||
.map((e) => this.hass!.states[e.entity_id]?.attributes.device_class || "")
|
.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)];
|
return [...new Set(classes)];
|
||||||
});
|
}
|
||||||
|
|
||||||
private _buildOptions = memoizeOne(
|
private _buildBinaryOptions = memoizeOne(
|
||||||
(possibleClasses: string[], currentClasses: string[]): SelectOption[] => {
|
(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(
|
const options = [...new Set([...possibleClasses, ...currentClasses])].map(
|
||||||
(deviceClass) => ({
|
(deviceClass) => ({
|
||||||
value: deviceClass,
|
value: deviceClass,
|
||||||
label:
|
label:
|
||||||
this.hass!.localize(
|
this.hass!.localize(
|
||||||
`component.binary_sensor.entity_component.${deviceClass}.name`
|
`component.${domain}.entity_component.${deviceClass}.name`
|
||||||
) || deviceClass,
|
) || deviceClass,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
options.sort((a, b) =>
|
options.sort((a, b) =>
|
||||||
caseInsensitiveStringCompare(
|
caseInsensitiveStringCompare(a.label, b.label, this.hass!.locale.language)
|
||||||
a.label,
|
|
||||||
b.label,
|
|
||||||
this.hass!.locale.language
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
public setConfig(config: AreaCardConfig): void {
|
public setConfig(config: AreaCardConfig): void {
|
||||||
assert(config, cardConfigStruct);
|
assert(config, cardConfigStruct);
|
||||||
this._config = config;
|
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() {
|
protected render() {
|
||||||
if (!this.hass || !this._config) {
|
if (!this.hass || !this._config) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
const possibleClasses = this._binaryClassesForArea(this._config.area || "");
|
const possibleBinaryClasses = this._binaryClassesForArea(
|
||||||
const selectOptions = this._buildOptions(
|
this._config.area || ""
|
||||||
possibleClasses,
|
);
|
||||||
|
const possibleSensorClasses = this._sensorClassesForArea(
|
||||||
|
this._config.area || "",
|
||||||
|
this._numericDeviceClasses
|
||||||
|
);
|
||||||
|
const binarySelectOptions = this._buildBinaryOptions(
|
||||||
|
possibleBinaryClasses,
|
||||||
this._config.alert_classes || DEVICE_CLASSES.binary_sensor
|
this._config.alert_classes || DEVICE_CLASSES.binary_sensor
|
||||||
);
|
);
|
||||||
|
const sensorSelectOptions = this._buildSensorOptions(
|
||||||
|
possibleSensorClasses,
|
||||||
|
this._config.sensor_classes || DEVICE_CLASSES.sensor
|
||||||
|
);
|
||||||
|
|
||||||
const schema = this._schema(
|
const schema = this._schema(
|
||||||
this._config.show_camera || false,
|
this._config.show_camera || false,
|
||||||
selectOptions
|
binarySelectOptions,
|
||||||
|
sensorSelectOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
camera_view: "auto",
|
camera_view: "auto",
|
||||||
alert_classes: DEVICE_CLASSES.binary_sensor,
|
alert_classes: DEVICE_CLASSES.binary_sensor,
|
||||||
|
sensor_classes: DEVICE_CLASSES.sensor,
|
||||||
...this._config,
|
...this._config,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -201,10 +267,6 @@ export class HuiAreaCardEditor
|
|||||||
return this.hass!.localize(
|
return this.hass!.localize(
|
||||||
"ui.panel.lovelace.editor.card.generic.camera_view"
|
"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(
|
return this.hass!.localize(
|
||||||
`ui.panel.lovelace.editor.card.area.${schema.name}`
|
`ui.panel.lovelace.editor.card.area.${schema.name}`
|
||||||
|
@ -5133,6 +5133,7 @@
|
|||||||
"area": {
|
"area": {
|
||||||
"name": "Area",
|
"name": "Area",
|
||||||
"alert_classes": "Alert Classes",
|
"alert_classes": "Alert Classes",
|
||||||
|
"sensor_classes": "Sensor Classes",
|
||||||
"description": "The Area card automatically displays entities of a specific area.",
|
"description": "The Area card automatically displays entities of a specific area.",
|
||||||
"show_camera": "Show camera feed instead of area picture"
|
"show_camera": "Show camera feed instead of area picture"
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user