diff --git a/src/panels/lovelace/cards/hui-area-card.ts b/src/panels/lovelace/cards/hui-area-card.ts index e9a72c7d16..abe4897942 100644 --- a/src/panels/lovelace/cards/hui-area-card.ts +++ b/src/panels/lovelace/cards/hui-area-card.ts @@ -1,4 +1,12 @@ import "@material/mwc-ripple"; +import { + mdiLightbulbMultiple, + mdiLightbulbMultipleOff, + mdiRun, + mdiToggleSwitch, + mdiToggleSwitchOff, + mdiWaterPercent, +} from "@mdi/js"; import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, @@ -10,13 +18,13 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; +import { STATES_OFF } from "../../../common/const"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; -import { fireEvent } from "../../../common/dom/fire_event"; import { computeDomain } from "../../../common/entity/compute_domain"; -import { computeStateDisplay } from "../../../common/entity/compute_state_display"; +import { domainIcon } from "../../../common/entity/domain_icon"; import { navigate } from "../../../common/navigate"; +import { formatNumber } from "../../../common/number/format_number"; import "../../../components/entity/state-badge"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; @@ -30,31 +38,40 @@ import { DeviceRegistryEntry, subscribeDeviceRegistry, } from "../../../data/device_registry"; +import { UNAVAILABLE_STATES } from "../../../data/entity"; import { EntityRegistryEntry, subscribeEntityRegistry, } from "../../../data/entity_registry"; import { forwardHaptic } from "../../../data/haptics"; -import { ActionHandlerEvent } from "../../../data/lovelace"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../types"; -import { actionHandler } from "../common/directives/action-handler-directive"; -import { toggleEntity } from "../common/entity/toggle-entity"; import "../components/hui-warning"; import { LovelaceCard, LovelaceCardEditor } from "../types"; import { AreaCardConfig } from "./types"; -const SENSOR_DOMAINS = new Set(["sensor", "binary_sensor"]); +const SENSOR_DOMAINS = ["sensor"]; -const SENSOR_DEVICE_CLASSES = new Set([ - "temperature", - "humidity", - "motion", - "door", - "aqi", -]); +const ALERT_DOMAINS = ["binary_sensor"]; -const TOGGLE_DOMAINS = new Set(["light", "fan", "switch"]); +const TOGGLE_DOMAINS = ["light", "switch", "fan"]; + +const OTHER_DOMAINS = ["camera"]; + +const DEVICE_CLASSES = { + sensor: ["temperature"], + binary_sensor: ["motion"], +}; + +const DOMAIN_ICONS = { + light: { on: mdiLightbulbMultiple, off: mdiLightbulbMultipleOff }, + switch: { on: mdiToggleSwitch, off: mdiToggleSwitchOff }, + fan: { on: domainIcon("fan"), off: domainIcon("fan") }, + sensor: { humidity: mdiWaterPercent }, + binary_sensor: { + motion: mdiRun, + }, +}; @customElement("hui-area-card") export class HuiAreaCard @@ -80,7 +97,7 @@ export class HuiAreaCard @state() private _areas?: AreaRegistryEntry[]; - private _memberships = memoizeOne( + private _entitiesByDomain = memoizeOne( ( areaId: string, devicesInArea: Set, @@ -97,44 +114,98 @@ export class HuiAreaCard ) .map((entry) => entry.entity_id); - const sensorEntities: HassEntity[] = []; - const entitiesToggle: HassEntity[] = []; + const entitiesByDomain: { [domain: string]: HassEntity[] } = {}; for (const entity of entitiesInArea) { const domain = computeDomain(entity); - if (!TOGGLE_DOMAINS.has(domain) && !SENSOR_DOMAINS.has(domain)) { + if ( + !TOGGLE_DOMAINS.includes(domain) && + !SENSOR_DOMAINS.includes(domain) && + !ALERT_DOMAINS.includes(domain) && + !OTHER_DOMAINS.includes(domain) + ) { continue; } - const stateObj: HassEntity | undefined = states[entity]; if (!stateObj) { continue; } - if (entitiesToggle.length < 3 && TOGGLE_DOMAINS.has(domain)) { - entitiesToggle.push(stateObj); + if ( + (SENSOR_DOMAINS.includes(domain) || ALERT_DOMAINS.includes(domain)) && + !DEVICE_CLASSES[domain].includes( + stateObj.attributes.device_class || "" + ) + ) { continue; } - if ( - sensorEntities.length < 3 && - SENSOR_DOMAINS.has(domain) && - stateObj.attributes.device_class && - SENSOR_DEVICE_CLASSES.has(stateObj.attributes.device_class) - ) { - sensorEntities.push(stateObj); - } - - if (sensorEntities.length === 3 && entitiesToggle.length === 3) { - break; + if (!(domain in entitiesByDomain)) { + entitiesByDomain[domain] = []; } + entitiesByDomain[domain].push(stateObj); } - return { sensorEntities, entitiesToggle }; + return entitiesByDomain; } ); + private _isOn(domain: string, deviceClass?: string): boolean | undefined { + const entities = this._entitiesByDomain( + this._config!.area, + this._devicesInArea(this._config!.area, this._devices!), + this._entities!, + this.hass.states + )[domain]; + if (!entities) { + return undefined; + } + return ( + deviceClass + ? entities.filter( + (entity) => entity.attributes.device_class === deviceClass + ) + : entities + ).some( + (entity) => + !UNAVAILABLE_STATES.includes(entity.state) && + !STATES_OFF.includes(entity.state) + ); + } + + private _average(domain: string, deviceClass?: string): string | undefined { + const entities = this._entitiesByDomain( + this._config!.area, + this._devicesInArea(this._config!.area, this._devices!), + this._entities!, + this.hass.states + )[domain].filter((entity) => + deviceClass ? entity.attributes.device_class === deviceClass : true + ); + if (!entities) { + return undefined; + } + let uom; + const values = entities.filter((entity) => { + if (!entity.attributes.unit_of_measurement) { + return false; + } + if (!uom) { + uom = entity.attributes.unit_of_measurement; + return true; + } + return entity.attributes.unit_of_measurement === uom; + }); + if (!values.length) { + return undefined; + } + const sum = values.reduce((a, b) => a + Number(b.state), 0); + return `${formatNumber(sum / values.length, this.hass!.locale, { + maximumFractionDigits: 1, + })} ${uom}`; + } + private _area = memoizeOne( (areaId: string | undefined, areas: AreaRegistryEntry[]) => areas.find((area) => area.area_id === areaId) || null @@ -212,22 +283,18 @@ export class HuiAreaCard return false; } - const { sensorEntities, entitiesToggle } = this._memberships( + const entities = this._entitiesByDomain( this._config.area, this._devicesInArea(this._config.area, this._devices), this._entities, this.hass.states ); - for (const stateObj of sensorEntities) { - if (oldHass!.states[stateObj.entity_id] !== stateObj) { - return true; - } - } - - for (const stateObj of entitiesToggle) { - if (oldHass!.states[stateObj.entity_id] !== stateObj) { - return true; + for (const domainEntities of Object.values(entities)) { + for (const stateObj of domainEntities) { + if (oldHass!.states[stateObj.entity_id] !== stateObj) { + return true; + } } } @@ -245,13 +312,12 @@ export class HuiAreaCard return html``; } - const { sensorEntities, entitiesToggle } = this._memberships( + const entitiesByDomain = this._entitiesByDomain( this._config.area, this._devicesInArea(this._config.area, this._devices), this._entities, this.hass.states ); - const area = this._area(this._config.area, this._areas); if (area === null) { @@ -262,62 +328,98 @@ export class HuiAreaCard `; } + const sensors: TemplateResult[] = []; + SENSOR_DOMAINS.forEach((domain) => { + if (!(domain in entitiesByDomain)) { + return; + } + DEVICE_CLASSES[domain].forEach((deviceClass) => { + if ( + entitiesByDomain[domain].some( + (entity) => entity.attributes.device_class === deviceClass + ) + ) { + sensors.push(html` + ${DOMAIN_ICONS[domain][deviceClass] + ? html`` + : ""} + ${this._average(domain, deviceClass)} + `); + } + }); + }); + + let cameraEntityId: string | undefined; + if ("camera" in entitiesByDomain) { + cameraEntityId = entitiesByDomain.camera[0].entity_id; + } + return html` - -
-
- ${sensorEntities.map( - (stateObj) => html` - - - ${computeDomain(stateObj.entity_id) === "binary_sensor" - ? "" - : html` - ${computeStateDisplay( - this.hass!.localize, - stateObj, - this.hass!.locale - )} - `} - - ` - )} + + ${area.picture || cameraEntityId + ? html`` + : ""} + +