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:
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user