mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
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:
parent
2053cf23c2
commit
9f26bedf51
@ -26,6 +26,7 @@ import memoizeOne from "memoize-one";
|
|||||||
import { STATES_OFF } from "../../../common/const";
|
import { STATES_OFF } 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 { 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 } from "../../../common/number/format_number";
|
||||||
@ -67,7 +68,7 @@ const TOGGLE_DOMAINS = ["light", "switch", "fan"];
|
|||||||
|
|
||||||
const OTHER_DOMAINS = ["camera"];
|
const OTHER_DOMAINS = ["camera"];
|
||||||
|
|
||||||
const DEVICE_CLASSES = {
|
export const DEVICE_CLASSES = {
|
||||||
sensor: ["temperature", "humidity"],
|
sensor: ["temperature", "humidity"],
|
||||||
binary_sensor: ["motion", "moisture"],
|
binary_sensor: ["motion", "moisture"],
|
||||||
};
|
};
|
||||||
@ -113,6 +114,8 @@ export class HuiAreaCard
|
|||||||
|
|
||||||
@state() private _areas?: AreaRegistryEntry[];
|
@state() private _areas?: AreaRegistryEntry[];
|
||||||
|
|
||||||
|
private _deviceClasses: { [key: string]: string[] } = DEVICE_CLASSES;
|
||||||
|
|
||||||
private _ratio: {
|
private _ratio: {
|
||||||
w: number;
|
w: number;
|
||||||
h: number;
|
h: number;
|
||||||
@ -123,6 +126,7 @@ export class HuiAreaCard
|
|||||||
areaId: string,
|
areaId: string,
|
||||||
devicesInArea: Set<string>,
|
devicesInArea: Set<string>,
|
||||||
registryEntities: EntityRegistryEntry[],
|
registryEntities: EntityRegistryEntry[],
|
||||||
|
deviceClasses: { [key: string]: string[] },
|
||||||
states: HomeAssistant["states"]
|
states: HomeAssistant["states"]
|
||||||
) => {
|
) => {
|
||||||
const entitiesInArea = registryEntities
|
const entitiesInArea = registryEntities
|
||||||
@ -156,7 +160,7 @@ export class HuiAreaCard
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
(SENSOR_DOMAINS.includes(domain) || ALERT_DOMAINS.includes(domain)) &&
|
(SENSOR_DOMAINS.includes(domain) || ALERT_DOMAINS.includes(domain)) &&
|
||||||
!DEVICE_CLASSES[domain].includes(
|
!deviceClasses[domain].includes(
|
||||||
stateObj.attributes.device_class || ""
|
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(
|
const entities = this._entitiesByDomain(
|
||||||
this._config!.area,
|
this._config!.area,
|
||||||
this._devicesInArea(this._config!.area, this._devices!),
|
this._devicesInArea(this._config!.area, this._devices!),
|
||||||
this._entities!,
|
this._entities!,
|
||||||
|
this._deviceClasses,
|
||||||
this.hass.states
|
this.hass.states
|
||||||
)[domain];
|
)[domain];
|
||||||
if (!entities) {
|
if (!entities) {
|
||||||
@ -189,7 +194,7 @@ export class HuiAreaCard
|
|||||||
(entity) => entity.attributes.device_class === deviceClass
|
(entity) => entity.attributes.device_class === deviceClass
|
||||||
)
|
)
|
||||||
: entities
|
: entities
|
||||||
).some(
|
).find(
|
||||||
(entity) =>
|
(entity) =>
|
||||||
!isUnavailableState(entity.state) && !STATES_OFF.includes(entity.state)
|
!isUnavailableState(entity.state) && !STATES_OFF.includes(entity.state)
|
||||||
);
|
);
|
||||||
@ -200,6 +205,7 @@ export class HuiAreaCard
|
|||||||
this._config!.area,
|
this._config!.area,
|
||||||
this._devicesInArea(this._config!.area, this._devices!),
|
this._devicesInArea(this._config!.area, this._devices!),
|
||||||
this._entities!,
|
this._entities!,
|
||||||
|
this._deviceClasses,
|
||||||
this.hass.states
|
this.hass.states
|
||||||
)[domain].filter((entity) =>
|
)[domain].filter((entity) =>
|
||||||
deviceClass ? entity.attributes.device_class === deviceClass : true
|
deviceClass ? entity.attributes.device_class === deviceClass : true
|
||||||
@ -273,6 +279,11 @@ export class HuiAreaCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._config = config;
|
this._config = config;
|
||||||
|
|
||||||
|
this._deviceClasses = { ...DEVICE_CLASSES };
|
||||||
|
if (config.alert_classes) {
|
||||||
|
this._deviceClasses.binary_sensor = config.alert_classes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
@ -314,6 +325,7 @@ export class HuiAreaCard
|
|||||||
this._config.area,
|
this._config.area,
|
||||||
this._devicesInArea(this._config.area, this._devices),
|
this._devicesInArea(this._config.area, this._devices),
|
||||||
this._entities,
|
this._entities,
|
||||||
|
this._deviceClasses,
|
||||||
this.hass.states
|
this.hass.states
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -355,6 +367,7 @@ export class HuiAreaCard
|
|||||||
this._config.area,
|
this._config.area,
|
||||||
this._devicesInArea(this._config.area, this._devices),
|
this._devicesInArea(this._config.area, this._devices),
|
||||||
this._entities,
|
this._entities,
|
||||||
|
this._deviceClasses,
|
||||||
this.hass.states
|
this.hass.states
|
||||||
);
|
);
|
||||||
const area = this._area(this._config.area, this._areas);
|
const area = this._area(this._config.area, this._areas);
|
||||||
@ -427,17 +440,16 @@ export class HuiAreaCard
|
|||||||
if (!(domain in entitiesByDomain)) {
|
if (!(domain in entitiesByDomain)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return DEVICE_CLASSES[domain].map((deviceClass) =>
|
return this._deviceClasses[domain].map((deviceClass) => {
|
||||||
this._isOn(domain, deviceClass)
|
const entity = this._isOn(domain, deviceClass);
|
||||||
? html`
|
return entity
|
||||||
${DOMAIN_ICONS[domain][deviceClass]
|
? html`<ha-svg-icon
|
||||||
? html`<ha-svg-icon
|
class="alert"
|
||||||
.path=${DOMAIN_ICONS[domain][deviceClass]}
|
.path=${DOMAIN_ICONS[domain][deviceClass] ||
|
||||||
></ha-svg-icon>`
|
binarySensorIcon(entity.state, entity)}
|
||||||
: ""}
|
></ha-svg-icon>`
|
||||||
`
|
: nothing;
|
||||||
: ""
|
});
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
@ -562,6 +574,7 @@ export class HuiAreaCard
|
|||||||
background: var(--accent-color);
|
background: var(--accent-color);
|
||||||
color: var(--text-accent-color, var(--text-primary-color));
|
color: var(--text-accent-color, var(--text-primary-color));
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,29 @@
|
|||||||
import { html, LitElement, nothing } from "lit";
|
import { html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
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 { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-form/ha-form";
|
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 { SchemaUnion } from "../../../../components/ha-form/types";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import type { AreaCardConfig } from "../../cards/types";
|
import type { AreaCardConfig } from "../../cards/types";
|
||||||
import type { LovelaceCardEditor } from "../../types";
|
import type { LovelaceCardEditor } from "../../types";
|
||||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
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(
|
const cardConfigStruct = assign(
|
||||||
baseLovelaceCardConfig,
|
baseLovelaceCardConfig,
|
||||||
@ -20,8 +34,10 @@ const cardConfigStruct = assign(
|
|||||||
show_camera: optional(boolean()),
|
show_camera: optional(boolean()),
|
||||||
camera_view: optional(string()),
|
camera_view: optional(string()),
|
||||||
aspect_ratio: optional(string()),
|
aspect_ratio: optional(string()),
|
||||||
|
alert_classes: optional(array(string())),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@customElement("hui-area-card-editor")
|
@customElement("hui-area-card-editor")
|
||||||
export class HuiAreaCardEditor
|
export class HuiAreaCardEditor
|
||||||
extends LitElement
|
extends LitElement
|
||||||
@ -32,7 +48,7 @@ export class HuiAreaCardEditor
|
|||||||
@state() private _config?: AreaCardConfig;
|
@state() private _config?: AreaCardConfig;
|
||||||
|
|
||||||
private _schema = memoizeOne(
|
private _schema = memoizeOne(
|
||||||
(showCamera: boolean) =>
|
(showCamera: boolean, binaryClasses: SelectOption[]) =>
|
||||||
[
|
[
|
||||||
{ name: "area", selector: { area: {} } },
|
{ name: "area", selector: { area: {} } },
|
||||||
{ name: "show_camera", required: false, selector: { boolean: {} } },
|
{ 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
|
] 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 {
|
public setConfig(config: AreaCardConfig): void {
|
||||||
assert(config, cardConfigStruct);
|
assert(config, cardConfigStruct);
|
||||||
this._config = config;
|
this._config = config;
|
||||||
@ -74,10 +141,20 @@ export class HuiAreaCardEditor
|
|||||||
return nothing;
|
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 = {
|
const data = {
|
||||||
camera_view: "auto",
|
camera_view: "auto",
|
||||||
|
alert_classes: DEVICE_CLASSES.binary_sensor,
|
||||||
...this._config,
|
...this._config,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -124,6 +201,10 @@ 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}`
|
||||||
|
@ -5132,6 +5132,7 @@
|
|||||||
},
|
},
|
||||||
"area": {
|
"area": {
|
||||||
"name": "Area",
|
"name": "Area",
|
||||||
|
"alert_classes": "Alert 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