Custom device_classes for alert icons in Area Card (#19131)

* Custom device_classes for alerts in Area Card

* small refactor

* drop more-info feature

* Update src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* localization and css updates

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
karwosts 2024-01-15 06:51:19 -08:00 committed by GitHub
parent 2053cf23c2
commit 9f26bedf51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 114 additions and 19 deletions

View File

@ -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<string>,
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`<ha-svg-icon
.path=${DOMAIN_ICONS[domain][deviceClass]}
></ha-svg-icon>`
: ""}
`
: ""
);
return this._deviceClasses[domain].map((deviceClass) => {
const entity = this._isOn(domain, deviceClass);
return entity
? html`<ha-svg-icon
class="alert"
.path=${DOMAIN_ICONS[domain][deviceClass] ||
binarySensorIcon(entity.state, entity)}
></ha-svg-icon>`
: nothing;
});
})}
</div>
<div class="bottom">
@ -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%;
}

View File

@ -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}`

View File

@ -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"
},