diff --git a/src/panels/lovelace/cards/hui-area-card.ts b/src/panels/lovelace/cards/hui-area-card.ts index eb80dbd2f5..7fe1749099 100644 --- a/src/panels/lovelace/cards/hui-area-card.ts +++ b/src/panels/lovelace/cards/hui-area-card.ts @@ -26,6 +26,7 @@ import memoizeOne from "memoize-one"; import { STATES_OFF } 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"; @@ -67,7 +68,7 @@ const TOGGLE_DOMAINS = ["light", "switch", "fan"]; const OTHER_DOMAINS = ["camera"]; -const DEVICE_CLASSES = { +export const DEVICE_CLASSES = { sensor: ["temperature", "humidity"], binary_sensor: ["motion", "moisture"], }; @@ -113,6 +114,8 @@ export class HuiAreaCard @state() private _areas?: AreaRegistryEntry[]; + private _deviceClasses: { [key: string]: string[] } = DEVICE_CLASSES; + private _ratio: { w: number; h: number; @@ -123,6 +126,7 @@ export class HuiAreaCard areaId: string, devicesInArea: Set, registryEntities: EntityRegistryEntry[], + deviceClasses: { [key: string]: string[] }, states: HomeAssistant["states"] ) => { const entitiesInArea = registryEntities @@ -156,7 +160,7 @@ export class HuiAreaCard if ( (SENSOR_DOMAINS.includes(domain) || ALERT_DOMAINS.includes(domain)) && - !DEVICE_CLASSES[domain].includes( + !deviceClasses[domain].includes( stateObj.attributes.device_class || "" ) ) { @@ -173,11 +177,12 @@ export class HuiAreaCard } ); - private _isOn(domain: string, deviceClass?: string): boolean | undefined { + private _isOn(domain: string, deviceClass?: string): HassEntity | undefined { const entities = this._entitiesByDomain( this._config!.area, this._devicesInArea(this._config!.area, this._devices!), this._entities!, + this._deviceClasses, this.hass.states )[domain]; if (!entities) { @@ -189,7 +194,7 @@ export class HuiAreaCard (entity) => entity.attributes.device_class === deviceClass ) : entities - ).some( + ).find( (entity) => !isUnavailableState(entity.state) && !STATES_OFF.includes(entity.state) ); @@ -200,6 +205,7 @@ export class HuiAreaCard this._config!.area, this._devicesInArea(this._config!.area, this._devices!), this._entities!, + this._deviceClasses, this.hass.states )[domain].filter((entity) => deviceClass ? entity.attributes.device_class === deviceClass : true @@ -273,6 +279,11 @@ export class HuiAreaCard } this._config = config; + + this._deviceClasses = { ...DEVICE_CLASSES }; + if (config.alert_classes) { + this._deviceClasses.binary_sensor = config.alert_classes; + } } protected shouldUpdate(changedProps: PropertyValues): boolean { @@ -314,6 +325,7 @@ export class HuiAreaCard this._config.area, this._devicesInArea(this._config.area, this._devices), this._entities, + this._deviceClasses, this.hass.states ); @@ -355,6 +367,7 @@ export class HuiAreaCard this._config.area, this._devicesInArea(this._config.area, this._devices), this._entities, + this._deviceClasses, this.hass.states ); const area = this._area(this._config.area, this._areas); @@ -427,17 +440,16 @@ export class HuiAreaCard if (!(domain in entitiesByDomain)) { return ""; } - return DEVICE_CLASSES[domain].map((deviceClass) => - this._isOn(domain, deviceClass) - ? html` - ${DOMAIN_ICONS[domain][deviceClass] - ? html`` - : ""} - ` - : "" - ); + return this._deviceClasses[domain].map((deviceClass) => { + const entity = this._isOn(domain, deviceClass); + return entity + ? html`` + : nothing; + }); })}
@@ -562,6 +574,7 @@ export class HuiAreaCard background: var(--accent-color); color: var(--text-accent-color, var(--text-primary-color)); padding: 8px; + margin-right: 8px; border-radius: 50%; } 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 c28567945a..c49e9bf99e 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 @@ -1,15 +1,29 @@ import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { assert, assign, boolean, object, optional, string } from "superstruct"; +import { + assert, + array, + assign, + boolean, + object, + optional, + string, +} from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-form/ha-form"; -import { DEFAULT_ASPECT_RATIO } from "../../cards/hui-area-card"; +import { + DEFAULT_ASPECT_RATIO, + DEVICE_CLASSES, +} from "../../cards/hui-area-card"; import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../types"; import type { AreaCardConfig } from "../../cards/types"; import type { LovelaceCardEditor } from "../../types"; 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"; const cardConfigStruct = assign( baseLovelaceCardConfig, @@ -20,8 +34,10 @@ const cardConfigStruct = assign( show_camera: optional(boolean()), camera_view: optional(string()), aspect_ratio: optional(string()), + alert_classes: optional(array(string())), }) ); + @customElement("hui-area-card-editor") export class HuiAreaCardEditor extends LitElement @@ -32,7 +48,7 @@ export class HuiAreaCardEditor @state() private _config?: AreaCardConfig; private _schema = memoizeOne( - (showCamera: boolean) => + (showCamera: boolean, binaryClasses: SelectOption[]) => [ { name: "area", selector: { area: {} } }, { name: "show_camera", required: false, selector: { boolean: {} } }, @@ -61,9 +77,60 @@ export class HuiAreaCardEditor }, ], }, + { + name: "alert_classes", + selector: { + select: { + reorder: true, + multiple: true, + custom_value: true, + options: binaryClasses, + }, + }, + }, ] as const ); + private _binaryClassesForArea = memoizeOne((area: string): string[] => { + const entities = Object.values(this.hass!.entities).filter( + (e) => + computeDomain(e.entity_id) === "binary_sensor" && + !e.entity_category && + !e.hidden && + (e.area_id === area || + (e.device_id && this.hass!.devices[e.device_id].area_id === area)) + ); + + const classes = entities + .map((e) => this.hass!.states[e.entity_id]?.attributes.device_class || "") + .filter((c) => 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; + } + ); + public setConfig(config: AreaCardConfig): void { assert(config, cardConfigStruct); this._config = config; @@ -74,10 +141,20 @@ export class HuiAreaCardEditor return nothing; } - const schema = this._schema(this._config.show_camera || false); + const possibleClasses = this._binaryClassesForArea(this._config.area || ""); + const selectOptions = this._buildOptions( + possibleClasses, + this._config.alert_classes || DEVICE_CLASSES.binary_sensor + ); + + const schema = this._schema( + this._config.show_camera || false, + selectOptions + ); const data = { camera_view: "auto", + alert_classes: DEVICE_CLASSES.binary_sensor, ...this._config, }; @@ -124,6 +201,10 @@ 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 0ef5836078..6ea99bc0ad 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5132,6 +5132,7 @@ }, "area": { "name": "Area", + "alert_classes": "Alert Classes", "description": "The Area card automatically displays entities of a specific area.", "show_camera": "Show camera feed instead of area picture" },