Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sensor summation to area card #20749

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
192 changes: 128 additions & 64 deletions src/panels/lovelace/cards/hui-area-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,61 @@ const TOGGLE_DOMAINS = ["light", "switch", "fan"];

const OTHER_DOMAINS = ["camera"];

export const DEVICE_CLASSES = {
sensor: ["temperature", "humidity"],
binary_sensor: ["motion", "moisture"],
const ALL_DOMAINS = [
...SENSOR_DOMAINS,
...ALERT_DOMAINS,
...TOGGLE_DOMAINS,
...OTHER_DOMAINS,
];

const AVG_SENSOR_CLASSES = ["temperature", "humidity"];

const SUM_SENSOR_CLASSES = ["power", "energy", "volume"];

const BINARY_SENSOR_CLASSES = ["motion", "moisture"];

const SENSOR_CLASSES = [
...AVG_SENSOR_CLASSES,
...SUM_SENSOR_CLASSES,
...BINARY_SENSOR_CLASSES,
];

type DeviceClassOptions = {
sensor_type: string;
default_display_function: (values: number[]) => number;
};

type DeviceClasses = { [key: string]: DeviceClassOptions };

export const deviceClassesByDomain = (
deviceClasses: DeviceClasses,
domain: string
): string[] =>
Object.entries(deviceClasses)
.filter(([_deviceClass, opts]) => opts.sensor_type === domain)
.map(([deviceClass, _opts]) => deviceClass);

const sumValues = (values: number[]): number =>
values.reduce((total, value) => total + value, 0);

const avgValues = (values: number[]): number =>
sumValues(values) / values.length;

export const DEVICE_CLASSES: { [key: string]: DeviceClassOptions } = {
...SENSOR_CLASSES.reduce(
(acc, dc) => ({
...acc,
[dc]: {
sensor_type: BINARY_SENSOR_CLASSES.includes(dc)
? "binary_sensor"
: "sensor",
default_display_function: SUM_SENSOR_CLASSES.includes(dc)
? sumValues
: avgValues,
},
}),
{}
),
};

const DOMAIN_ICONS = {
Expand Down Expand Up @@ -110,7 +162,7 @@ export class HuiAreaCard

@state() private _areas?: AreaRegistryEntry[];

private _deviceClasses: { [key: string]: string[] } = DEVICE_CLASSES;
private _deviceClasses: DeviceClasses = DEVICE_CLASSES;

private _ratio: {
w: number;
Expand All @@ -122,7 +174,7 @@ export class HuiAreaCard
areaId: string,
devicesInArea: Set<string>,
registryEntities: EntityRegistryEntry[],
deviceClasses: { [key: string]: string[] },
deviceClasses: DeviceClasses,
states: HomeAssistant["states"]
) => {
const entitiesInArea = registryEntities
Expand All @@ -140,12 +192,7 @@ export class HuiAreaCard

for (const entity of entitiesInArea) {
const domain = computeDomain(entity);
if (
!TOGGLE_DOMAINS.includes(domain) &&
!SENSOR_DOMAINS.includes(domain) &&
!ALERT_DOMAINS.includes(domain) &&
!OTHER_DOMAINS.includes(domain)
) {
if (!ALL_DOMAINS.includes(domain)) {
continue;
}
const stateObj: HassEntity | undefined = states[entity];
Expand All @@ -155,8 +202,8 @@ export class HuiAreaCard
}

if (
(SENSOR_DOMAINS.includes(domain) || ALERT_DOMAINS.includes(domain)) &&
!deviceClasses[domain].includes(
[...SENSOR_DOMAINS, ...ALERT_DOMAINS].includes(domain) &&
!deviceClassesByDomain(deviceClasses, domain).includes(
stateObj.attributes.device_class || ""
)
) {
Expand Down Expand Up @@ -196,7 +243,10 @@ export class HuiAreaCard
);
}

private _average(domain: string, deviceClass?: string): string | undefined {
private _sensor_display_value(
domain: string,
deviceClass?: string
): string | undefined {
const entities = this._entitiesByDomain(
this._config!.area,
this._devicesInArea(this._config!.area, this._devices!),
Expand All @@ -210,24 +260,25 @@ export class HuiAreaCard
return undefined;
}
let uom;
const values = entities.filter((entity) => {
if (!isNumericState(entity) || isNaN(Number(entity.state))) {
return false;
}
if (!uom) {
uom = entity.attributes.unit_of_measurement;
return true;
}
return entity.attributes.unit_of_measurement === uom;
});
const values = entities
.filter((entity) => {
if (!isNumericState(entity) || isNaN(Number(entity.state))) {
return false;
}
if (!uom) {
uom = entity.attributes.unit_of_measurement;
return true;
}
return entity.attributes.unit_of_measurement === uom;
})
.map((entity) => Number(entity.state));
if (!values.length) {
return undefined;
}
const sum = values.reduce(
(total, entity) => total + Number(entity.state),
0
);
return `${formatNumber(sum / values.length, this.hass!.locale, {
const displayValue = deviceClass
? this._deviceClasses[deviceClass].default_display_function(values)
: "";
return `${formatNumber(displayValue, this.hass!.locale, {
maximumFractionDigits: 1,
})}${uom ? blankBeforeUnit(uom, this.hass!.locale) : ""}${uom || ""}`;
}
Expand Down Expand Up @@ -273,13 +324,22 @@ 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;
if (!config.sensor_classes && !config.alert_classes) {
this._deviceClasses = { ...DEVICE_CLASSES };
return;
}

this._deviceClasses = Object.entries(DEVICE_CLASSES)
.filter(([dc, _opts]) =>
[...config.sensor_classes, ...config.alert_classes].includes(dc)
)
.reduce(
(acc, [dc, opts]) => ({
...acc,
[dc]: opts,
}),
{}
);
}

protected shouldUpdate(changedProps: PropertyValues): boolean {
Expand Down Expand Up @@ -381,24 +441,26 @@ export class HuiAreaCard
if (!(domain in entitiesByDomain)) {
return;
}
this._deviceClasses[domain].forEach((deviceClass) => {
if (
entitiesByDomain[domain].some(
(entity) => entity.attributes.device_class === deviceClass
)
) {
sensors.push(html`
<div class="sensor">
<ha-domain-icon
.hass=${this.hass}
.domain=${domain}
.deviceClass=${deviceClass}
></ha-domain-icon>
${this._average(domain, deviceClass)}
</div>
`);
deviceClassesByDomain(this._deviceClasses, domain).forEach(
(deviceClass) => {
if (
entitiesByDomain[domain].some(
(entity) => entity.attributes.device_class === deviceClass
)
) {
sensors.push(html`
<div class="sensor">
<ha-domain-icon
.hass=${this.hass}
.domain=${domain}
.deviceClass=${deviceClass}
></ha-domain-icon>
${this._sensor_display_value(domain, deviceClass)}
</div>
`);
}
}
});
);
});

let cameraEntityId: string | undefined;
Expand Down Expand Up @@ -447,18 +509,20 @@ export class HuiAreaCard
if (!(domain in entitiesByDomain)) {
return nothing;
}
return this._deviceClasses[domain].map((deviceClass) => {
const entity = this._isOn(domain, deviceClass);
return entity
? html`
<ha-state-icon
class="alert"
.hass=${this.hass}
.stateObj=${entity}
></ha-state-icon>
`
: nothing;
});
return deviceClassesByDomain(this._deviceClasses, domain).map(
(deviceClass) => {
const entity = this._isOn(domain, deviceClass);
return entity
? html`
<ha-state-icon
class="alert"
.hass=${this.hass}
.stateObj=${entity}
></ha-state-icon>
`
: nothing;
}
);
})}
</div>
<div class="bottom">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import "../../../../components/ha-form/ha-form";
import {
DEFAULT_ASPECT_RATIO,
DEVICE_CLASSES,
deviceClassesByDomain,
} from "../../cards/hui-area-card";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
Expand Down Expand Up @@ -204,11 +205,13 @@ export class HuiAreaCardEditor
);
const binarySelectOptions = this._buildBinaryOptions(
possibleBinaryClasses,
this._config.alert_classes || DEVICE_CLASSES.binary_sensor
this._config.alert_classes ||
deviceClassesByDomain(DEVICE_CLASSES, "binary_sensor")
);
const sensorSelectOptions = this._buildSensorOptions(
possibleSensorClasses,
this._config.sensor_classes || DEVICE_CLASSES.sensor
this._config.sensor_classes ||
deviceClassesByDomain(DEVICE_CLASSES, "sensor")
);

const schema = this._schema(
Expand All @@ -219,8 +222,8 @@ export class HuiAreaCardEditor

const data = {
camera_view: "auto",
alert_classes: DEVICE_CLASSES.binary_sensor,
sensor_classes: DEVICE_CLASSES.sensor,
alert_classes: deviceClassesByDomain(DEVICE_CLASSES, "binary_sensor"),
sensor_classes: deviceClassesByDomain(DEVICE_CLASSES, "sensor"),
...this._config,
};

Expand Down