1
0
mirror of https://github.com/home-assistant/frontend.git synced 2025-12-20 02:38:53 +00:00

Use temp & humidity data from attributes in Area card

This commit is contained in:
Petar Petrov
2025-12-13 10:37:38 +02:00
parent abd706fed0
commit d6d0da7ba1

View File

@@ -67,6 +67,19 @@ export const SUM_DEVICE_CLASSES = [
"water", "water",
]; ];
// Additional sources for sensor device classes from entity attributes
// Maps device_class -> array of { domain, attribute } to include in aggregation
export const SENSOR_ATTRIBUTE_SOURCES: Record<
string,
{ domain: string; attribute: string }[]
> = {
temperature: [{ domain: "climate", attribute: "current_temperature" }],
humidity: [
{ domain: "climate", attribute: "current_humidity" },
{ domain: "humidifier", attribute: "current_humidity" },
],
};
export interface AreaCardFeatureContext extends LovelaceCardFeatureContext { export interface AreaCardFeatureContext extends LovelaceCardFeatureContext {
exclude_entities?: string[]; exclude_entities?: string[];
} }
@@ -251,6 +264,24 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
} }
); );
private _domainEntityIds = memoizeOne(
(
entities: HomeAssistant["entities"],
areaId: string,
domains: string[],
excludeEntities?: string[]
): string[] => {
const filter = generateEntityFilter(this.hass, {
area: areaId,
entity_category: "none",
domain: domains,
});
return Object.keys(entities).filter(
(id) => filter(id) && !excludeEntities?.includes(id)
);
}
);
private _computeActiveAlertStates(): HassEntity[] { private _computeActiveAlertStates(): HassEntity[] {
const areaId = this._config?.area; const areaId = this._config?.area;
const area = areaId ? this.hass.areas[areaId] : undefined; const area = areaId ? this.hass.areas[areaId] : undefined;
@@ -359,58 +390,73 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
: this.hass.formatEntityState(stateObj); : this.hass.formatEntityState(stateObj);
} }
const entityIds = groupedEntities.get(sensorClass); const sensorEntityIds = groupedEntities.get(sensorClass) || [];
const values: number[] = [];
let uom: string | undefined;
if (!entityIds) { for (const entityId of sensorEntityIds) {
return undefined; const stateObj = this.hass.states[entityId];
if (
stateObj &&
!isUnavailableState(stateObj.state) &&
isNumericState(stateObj) &&
!isNaN(Number(stateObj.state))
) {
if (!uom) {
uom = stateObj.attributes.unit_of_measurement;
}
if (stateObj.attributes.unit_of_measurement === uom) {
values.push(Number(stateObj.state));
}
}
} }
// Ensure all entities have state // Collect values from additional attribute sources
const entities = entityIds const attrSources = SENSOR_ATTRIBUTE_SOURCES[sensorClass];
.map((entityId) => this.hass.states[entityId]) if (attrSources) {
.filter(Boolean); const domains = [...new Set(attrSources.map((s) => s.domain))];
const attrEntityIds = this._domainEntityIds(
if (entities.length === 0) { this.hass.entities,
return undefined; area.area_id,
} domains,
excludeEntities
// If only one entity, return its formatted state
if (entities.length === 1) {
const stateObj = entities[0];
return isUnavailableState(stateObj.state)
? ""
: this.hass.formatEntityState(stateObj);
}
// Use the first entity's unit_of_measurement for formatting
const uom = entities.find(
(entity) => entity.attributes.unit_of_measurement
)?.attributes.unit_of_measurement;
// Ensure all entities have the same unit_of_measurement
const validEntities = entities.filter(
(entity) =>
entity.attributes.unit_of_measurement === uom &&
isNumericState(entity) &&
!isNaN(Number(entity.state))
); );
if (validEntities.length === 0) { for (const entityId of attrEntityIds) {
const stateObj = this.hass.states[entityId];
if (!stateObj) continue;
const domain = entityId.split(".")[0];
const source = attrSources.find((s) => s.domain === domain);
if (!source) continue;
const attrValue = stateObj.attributes[source.attribute];
if (attrValue == null || isNaN(Number(attrValue))) continue;
if (!uom) {
// Determine unit from attribute
uom = this._getAttributeUnit(sensorClass, domain);
}
values.push(Number(attrValue));
}
}
if (values.length === 0) {
return undefined; return undefined;
} }
const value = SUM_DEVICE_CLASSES.includes(sensorClass) const value = SUM_DEVICE_CLASSES.includes(sensorClass)
? this._computeSumState(validEntities) ? values.reduce((acc, v) => acc + v, 0)
: this._computeMedianState(validEntities); : this._computeMedianValue(values);
const formattedAverage = formatNumber(value, this.hass!.locale, { const formattedValue = formatNumber(value, this.hass.locale, {
maximumFractionDigits: 1, maximumFractionDigits: 1,
}); });
const formattedUnit = uom const formattedUnit = uom
? `${blankBeforeUnit(uom, this.hass!.locale)}${uom}` ? `${blankBeforeUnit(uom, this.hass.locale)}${uom}`
: ""; : "";
return `${formattedAverage}${formattedUnit}`; return `${formattedValue}${formattedUnit}`;
}) })
.filter(Boolean) .filter(Boolean)
.join(" · "); .join(" · ");
@@ -418,20 +464,25 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
return sensorStates; return sensorStates;
} }
private _computeSumState(entities: HassEntity[]): number { private _getAttributeUnit(sensorClass: string, domain: string): string {
return entities.reduce((acc, entity) => acc + Number(entity.state), 0); // Return the expected unit for attributes from specific domains
if (sensorClass === "temperature" && domain === "climate") {
return this.hass.config.unit_system.temperature;
}
if (sensorClass === "humidity") {
return "%";
}
return "";
} }
private _computeMedianState(entities: HassEntity[]): number { private _computeMedianValue(values: number[]): number {
const sortedStates = entities const sortedValues = [...values].sort((a, b) => a - b);
.map((entity) => Number(entity.state)) if (sortedValues.length % 2 === 0) {
.sort((a, b) => a - b); const medianIndex = sortedValues.length / 2;
if (sortedStates.length % 2 === 0) { return (sortedValues[medianIndex] + sortedValues[medianIndex - 1]) / 2;
const medianIndex = sortedStates.length / 2;
return (sortedStates[medianIndex] + sortedStates[medianIndex - 1]) / 2;
} }
const medianIndex = Math.floor(sortedStates.length / 2); const medianIndex = Math.floor(sortedValues.length / 2);
return sortedStates[medianIndex]; return sortedValues[medianIndex];
} }
private _featurePosition = memoizeOne((config: AreaCardConfig) => { private _featurePosition = memoizeOne((config: AreaCardConfig) => {