mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-30 04:36:36 +00:00
Add name and sensor states to area card
This commit is contained in:
parent
ebb98bd196
commit
c67024f36f
@ -1,367 +1,185 @@
|
||||
import {
|
||||
mdiFan,
|
||||
mdiFanOff,
|
||||
mdiLightbulbMultiple,
|
||||
mdiLightbulbMultipleOff,
|
||||
mdiRun,
|
||||
mdiToggleSwitch,
|
||||
mdiToggleSwitchOff,
|
||||
mdiWaterAlert,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { STATES_OFF } from "../../../common/const";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||
import { generateEntityFilter } from "../../../common/entity/entity_filter";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import {
|
||||
formatNumber,
|
||||
isNumericState,
|
||||
} from "../../../common/number/format_number";
|
||||
import { blankBeforeUnit } from "../../../common/translations/blank_before_unit";
|
||||
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
||||
import { subscribeOne } from "../../../common/util/subscribe-one";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-domain-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-state-icon";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { subscribeAreaRegistry } from "../../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import { subscribeDeviceRegistry } from "../../../data/device_registry";
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-ripple";
|
||||
import "../../../components/tile/ha-tile-icon";
|
||||
import "../../../components/tile/ha-tile-info";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/hui-image";
|
||||
import "../components/hui-warning";
|
||||
import type {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceGridOptions,
|
||||
} from "../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import type { AreaCardConfig } from "./types";
|
||||
|
||||
export const DEFAULT_ASPECT_RATIO = "16:9";
|
||||
|
||||
const SENSOR_DOMAINS = ["sensor"];
|
||||
|
||||
const ALERT_DOMAINS = ["binary_sensor"];
|
||||
|
||||
const TOGGLE_DOMAINS = ["light", "switch", "fan"];
|
||||
|
||||
const OTHER_DOMAINS = ["camera"];
|
||||
|
||||
export const DEVICE_CLASSES = {
|
||||
sensor: ["temperature", "humidity"],
|
||||
binary_sensor: ["motion", "moisture"],
|
||||
};
|
||||
|
||||
const DOMAIN_ICONS = {
|
||||
light: { on: mdiLightbulbMultiple, off: mdiLightbulbMultipleOff },
|
||||
switch: { on: mdiToggleSwitch, off: mdiToggleSwitchOff },
|
||||
fan: { on: mdiFan, off: mdiFanOff },
|
||||
binary_sensor: {
|
||||
motion: mdiRun,
|
||||
moisture: mdiWaterAlert,
|
||||
},
|
||||
};
|
||||
|
||||
@customElement("hui-area-card")
|
||||
export class HuiAreaCard
|
||||
extends SubscribeMixin(LitElement)
|
||||
implements LovelaceCard
|
||||
{
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
await import("../editor/config-elements/hui-area-card-editor");
|
||||
return document.createElement("hui-area-card-editor");
|
||||
}
|
||||
|
||||
public static async getStubConfig(
|
||||
hass: HomeAssistant
|
||||
): Promise<AreaCardConfig> {
|
||||
const areas = await subscribeOne(hass.connection, subscribeAreaRegistry);
|
||||
return { type: "area", area: areas[0]?.area_id || "" };
|
||||
}
|
||||
|
||||
export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public layout?: string;
|
||||
|
||||
@state() private _config?: AreaCardConfig;
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
@state() private _devices?: DeviceRegistryEntry[];
|
||||
|
||||
@state() private _areas?: AreaRegistryEntry[];
|
||||
|
||||
private _deviceClasses: Record<string, string[]> = DEVICE_CLASSES;
|
||||
|
||||
private _ratio: {
|
||||
w: number;
|
||||
h: number;
|
||||
} | null = null;
|
||||
|
||||
private _entitiesByDomain = memoizeOne(
|
||||
(
|
||||
areaId: string,
|
||||
devicesInArea: Set<string>,
|
||||
registryEntities: EntityRegistryEntry[],
|
||||
deviceClasses: Record<string, string[]>,
|
||||
states: HomeAssistant["states"]
|
||||
) => {
|
||||
const entitiesInArea = registryEntities
|
||||
.filter(
|
||||
(entry) =>
|
||||
!entry.entity_category &&
|
||||
!entry.hidden_by &&
|
||||
(entry.area_id
|
||||
? entry.area_id === areaId
|
||||
: entry.device_id && devicesInArea.has(entry.device_id))
|
||||
)
|
||||
.map((entry) => entry.entity_id);
|
||||
|
||||
const entitiesByDomain: Record<string, HassEntity[]> = {};
|
||||
|
||||
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)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const stateObj: HassEntity | undefined = states[entity];
|
||||
|
||||
if (!stateObj) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
(SENSOR_DOMAINS.includes(domain) || ALERT_DOMAINS.includes(domain)) &&
|
||||
!deviceClasses[domain].includes(
|
||||
stateObj.attributes.device_class || ""
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(domain in entitiesByDomain)) {
|
||||
entitiesByDomain[domain] = [];
|
||||
}
|
||||
entitiesByDomain[domain].push(stateObj);
|
||||
}
|
||||
|
||||
return entitiesByDomain;
|
||||
}
|
||||
);
|
||||
|
||||
private _isOn(domain: string, deviceClass?: string): HassEntity | undefined {
|
||||
const entities = this._entitiesByDomain(
|
||||
this._config!.area,
|
||||
this._devicesInArea(this._config!.area, this._devices!),
|
||||
this._entities!,
|
||||
this._deviceClasses,
|
||||
this.hass.states
|
||||
)[domain];
|
||||
if (!entities) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
deviceClass
|
||||
? entities.filter(
|
||||
(entity) => entity.attributes.device_class === deviceClass
|
||||
)
|
||||
: entities
|
||||
).find(
|
||||
(entity) =>
|
||||
!isUnavailableState(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._deviceClasses,
|
||||
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 (!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;
|
||||
});
|
||||
if (!values.length) {
|
||||
return undefined;
|
||||
}
|
||||
const sum = values.reduce(
|
||||
(total, entity) => total + Number(entity.state),
|
||||
0
|
||||
);
|
||||
return `${formatNumber(sum / values.length, this.hass!.locale, {
|
||||
maximumFractionDigits: 1,
|
||||
})}${uom ? blankBeforeUnit(uom, this.hass!.locale) : ""}${uom || ""}`;
|
||||
}
|
||||
|
||||
private _area = memoizeOne(
|
||||
(areaId: string | undefined, areas: AreaRegistryEntry[]) =>
|
||||
areas.find((area) => area.area_id === areaId) || null
|
||||
);
|
||||
|
||||
private _devicesInArea = memoizeOne(
|
||||
(areaId: string | undefined, devices: DeviceRegistryEntry[]) =>
|
||||
new Set(
|
||||
areaId
|
||||
? devices
|
||||
.filter((device) => device.area_id === areaId)
|
||||
.map((device) => device.id)
|
||||
: []
|
||||
)
|
||||
);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeAreaRegistry(this.hass!.connection, (areas) => {
|
||||
this._areas = areas;
|
||||
}),
|
||||
subscribeDeviceRegistry(this.hass!.connection, (devices) => {
|
||||
this._devices = devices;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass!.connection, (entries) => {
|
||||
this._entities = entries;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 3;
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
await import("../editor/config-elements/hui-area-card-editor");
|
||||
return document.createElement("hui-area-card-editor");
|
||||
}
|
||||
|
||||
public setConfig(config: AreaCardConfig): void {
|
||||
if (!config.area) {
|
||||
throw new Error("Area Required");
|
||||
}
|
||||
|
||||
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;
|
||||
public static async getStubConfig(
|
||||
hass: HomeAssistant
|
||||
): Promise<AreaCardConfig> {
|
||||
const areas = Object.values(hass.areas);
|
||||
return { type: "area-legacy", area: areas[0]?.area_id || "" };
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private get _hasCardAction() {
|
||||
return this._config?.navigation_path;
|
||||
}
|
||||
|
||||
private _handleAction() {
|
||||
if (this._config?.navigation_path) {
|
||||
navigate(this._config.navigation_path);
|
||||
}
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.has("_config") || !this._config) {
|
||||
return true;
|
||||
private _groupedSensorEntityIds = memoizeOne(
|
||||
(
|
||||
entities: HomeAssistant["entities"],
|
||||
areaId: string,
|
||||
sensorClasses: string[]
|
||||
): Map<string, string[]> => {
|
||||
const sensorFilter = generateEntityFilter(this.hass, {
|
||||
area: areaId,
|
||||
entity_category: "none",
|
||||
domain: "sensor",
|
||||
device_class: sensorClasses,
|
||||
});
|
||||
const entityIds = Object.keys(entities).filter(sensorFilter);
|
||||
|
||||
// Group entities by device class
|
||||
return entityIds.reduce((acc, entityId) => {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const deviceClass = stateObj.attributes.device_class!;
|
||||
if (!acc.has(deviceClass)) {
|
||||
acc.set(deviceClass, []);
|
||||
}
|
||||
acc.get(deviceClass)!.push(stateObj.entity_id);
|
||||
return acc;
|
||||
}, new Map<string, string[]>());
|
||||
}
|
||||
);
|
||||
|
||||
private _computeSensorsDisplay(): string | undefined {
|
||||
const areaId = this._config?.area;
|
||||
const area = areaId ? this.hass.areas[areaId] : undefined;
|
||||
const sensorClasses = this._config?.sensor_classes;
|
||||
if (!area || !sensorClasses) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
changedProps.has("_devicesInArea") ||
|
||||
changedProps.has("_areas") ||
|
||||
changedProps.has("_entities")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!changedProps.has("hass")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
|
||||
if (
|
||||
!oldHass ||
|
||||
oldHass.themes !== this.hass!.themes ||
|
||||
oldHass.locale !== this.hass!.locale
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
!this._devices ||
|
||||
!this._devicesInArea(this._config.area, this._devices) ||
|
||||
!this._entities
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const entities = this._entitiesByDomain(
|
||||
this._config.area,
|
||||
this._devicesInArea(this._config.area, this._devices),
|
||||
this._entities,
|
||||
this._deviceClasses,
|
||||
this.hass.states
|
||||
const groupedEntities = this._groupedSensorEntityIds(
|
||||
this.hass.entities,
|
||||
area.area_id,
|
||||
sensorClasses
|
||||
);
|
||||
|
||||
for (const domainEntities of Object.values(entities)) {
|
||||
for (const stateObj of domainEntities) {
|
||||
if (oldHass!.states[stateObj.entity_id] !== stateObj) {
|
||||
return true;
|
||||
const sensorStates = sensorClasses
|
||||
.map((sensorClass) => {
|
||||
if (sensorClass === "temperature" && area.temperature_entity_id) {
|
||||
const stateObj = this.hass.states[area.temperature_entity_id];
|
||||
return isUnavailableState(stateObj.state)
|
||||
? ""
|
||||
: this.hass.formatEntityState(stateObj);
|
||||
}
|
||||
if (sensorClass === "humidity" && area.humidity_entity_id) {
|
||||
const stateObj = this.hass.states[area.humidity_entity_id];
|
||||
return isUnavailableState(stateObj.state)
|
||||
? ""
|
||||
: this.hass.formatEntityState(stateObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
const entityIds = groupedEntities.get(sensorClass);
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (changedProps.has("_config") || this._ratio === null) {
|
||||
this._ratio = this._config?.aspect_ratio
|
||||
? parseAspectRatio(this._config?.aspect_ratio)
|
||||
: null;
|
||||
if (!entityIds) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this._ratio === null || this._ratio.w <= 0 || this._ratio.h <= 0) {
|
||||
this._ratio = parseAspectRatio(DEFAULT_ASPECT_RATIO);
|
||||
}
|
||||
}
|
||||
// Ensure all entities have state
|
||||
const entities = entityIds
|
||||
.map((entityId) => this.hass.states[entityId])
|
||||
.filter(Boolean);
|
||||
|
||||
if (entities.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const value =
|
||||
validEntities.reduce((acc, entity) => acc + Number(entity.state), 0) /
|
||||
validEntities.length;
|
||||
|
||||
const formattedAverage = formatNumber(value, this.hass!.locale, {
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
const formattedUnit = uom
|
||||
? `${blankBeforeUnit(uom, this.hass!.locale)}${uom}`
|
||||
: "";
|
||||
|
||||
return `${formattedAverage}${formattedUnit}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
|
||||
return sensorStates;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this._areas ||
|
||||
!this._devices ||
|
||||
!this._entities
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
const areaId = this._config?.area;
|
||||
const area = areaId ? this.hass.areas[areaId] : undefined;
|
||||
|
||||
const entitiesByDomain = this._entitiesByDomain(
|
||||
this._config.area,
|
||||
this._devicesInArea(this._config.area, this._devices),
|
||||
this._entities,
|
||||
this._deviceClasses,
|
||||
this.hass.states
|
||||
);
|
||||
const area = this._area(this._config.area, this._areas);
|
||||
|
||||
if (area === null) {
|
||||
if (!area) {
|
||||
return html`
|
||||
<hui-warning .hass=${this.hass}>
|
||||
${this.hass.localize("ui.card.area.area_not_found")}
|
||||
@ -369,315 +187,149 @@ export class HuiAreaCard
|
||||
`;
|
||||
}
|
||||
|
||||
const sensors: TemplateResult[] = [];
|
||||
SENSOR_DOMAINS.forEach((domain) => {
|
||||
if (!(domain in entitiesByDomain)) {
|
||||
return;
|
||||
}
|
||||
this._deviceClasses[domain].forEach((deviceClass) => {
|
||||
let areaSensorEntityId: string | null = null;
|
||||
switch (deviceClass) {
|
||||
case "temperature":
|
||||
areaSensorEntityId = area.temperature_entity_id;
|
||||
break;
|
||||
case "humidity":
|
||||
areaSensorEntityId = area.humidity_entity_id;
|
||||
break;
|
||||
}
|
||||
const areaEntity =
|
||||
areaSensorEntityId &&
|
||||
this.hass.states[areaSensorEntityId] &&
|
||||
!isUnavailableState(this.hass.states[areaSensorEntityId].state)
|
||||
? this.hass.states[areaSensorEntityId]
|
||||
: undefined;
|
||||
if (
|
||||
areaEntity ||
|
||||
entitiesByDomain[domain].some(
|
||||
(entity) => entity.attributes.device_class === deviceClass
|
||||
)
|
||||
) {
|
||||
let value = areaEntity
|
||||
? this.hass.formatEntityState(areaEntity)
|
||||
: this._average(domain, deviceClass);
|
||||
if (!value) value = "—";
|
||||
sensors.push(html`
|
||||
<div class="sensor">
|
||||
<ha-domain-icon
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
.deviceClass=${deviceClass}
|
||||
></ha-domain-icon>
|
||||
${value}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
});
|
||||
});
|
||||
const icon = area.icon;
|
||||
|
||||
let cameraEntityId: string | undefined;
|
||||
if (this._config.show_camera && "camera" in entitiesByDomain) {
|
||||
cameraEntityId = entitiesByDomain.camera[0].entity_id;
|
||||
}
|
||||
const name = computeAreaName(area);
|
||||
|
||||
const imageClass = area.picture || cameraEntityId;
|
||||
|
||||
const ignoreAspectRatio = this.layout === "grid";
|
||||
const primary = name;
|
||||
const secondary = this._computeSensorsDisplay();
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
class=${imageClass ? "image" : ""}
|
||||
style=${styleMap({
|
||||
paddingBottom:
|
||||
ignoreAspectRatio || imageClass
|
||||
? "0"
|
||||
: `${((100 * this._ratio!.h) / this._ratio!.w).toFixed(2)}%`,
|
||||
})}
|
||||
>
|
||||
${area.picture || cameraEntityId
|
||||
? html`
|
||||
<hui-image
|
||||
.config=${this._config}
|
||||
.hass=${this.hass}
|
||||
.image=${area.picture ? area.picture : undefined}
|
||||
.cameraImage=${cameraEntityId}
|
||||
.cameraView=${this._config.camera_view}
|
||||
.aspectRatio=${ignoreAspectRatio
|
||||
? undefined
|
||||
: this._config.aspect_ratio || DEFAULT_ASPECT_RATIO}
|
||||
fitMode="cover"
|
||||
></hui-image>
|
||||
`
|
||||
: area.icon
|
||||
? html`
|
||||
<div class="icon-container">
|
||||
<ha-icon icon=${area.icon}></ha-icon>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-card>
|
||||
<div
|
||||
class="container ${classMap({
|
||||
navigate: this._config.navigation_path !== undefined,
|
||||
})}"
|
||||
@click=${this._handleNavigation}
|
||||
class="background"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler()}
|
||||
role=${ifDefined(this._hasCardAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this._hasCardAction ? "0" : undefined)}
|
||||
aria-labelledby="info"
|
||||
>
|
||||
<div class="alerts">
|
||||
${ALERT_DOMAINS.map((domain) => {
|
||||
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;
|
||||
});
|
||||
})}
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div>
|
||||
<div class="name">${area.name}</div>
|
||||
${sensors.length
|
||||
? html`<div class="sensors">${sensors}</div>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
${TOGGLE_DOMAINS.map((domain) => {
|
||||
if (!(domain in entitiesByDomain)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const on = this._isOn(domain)!;
|
||||
return TOGGLE_DOMAINS.includes(domain)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class=${on ? "on" : "off"}
|
||||
.path=${DOMAIN_ICONS[domain][on ? "on" : "off"]}
|
||||
.domain=${domain}
|
||||
@click=${this._toggle}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: "";
|
||||
})}
|
||||
</div>
|
||||
<ha-ripple .disabled=${!this._hasCardAction}></ha-ripple>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<ha-tile-icon>
|
||||
${icon
|
||||
? html`<ha-icon slot="icon" .icon=${icon}></ha-icon>`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
</ha-tile-icon>
|
||||
<ha-tile-info
|
||||
id="info"
|
||||
.primary=${primary}
|
||||
.secondary=${secondary}
|
||||
></ha-tile-info>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const oldConfig = changedProps.get("_config") as AreaCardConfig | undefined;
|
||||
|
||||
if (
|
||||
(changedProps.has("hass") &&
|
||||
(!oldHass || oldHass.themes !== this.hass.themes)) ||
|
||||
(changedProps.has("_config") &&
|
||||
(!oldConfig || oldConfig.theme !== this._config.theme))
|
||||
) {
|
||||
applyThemesOnElement(this, this.hass.themes, this._config.theme);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleNavigation() {
|
||||
if (this._config!.navigation_path) {
|
||||
navigate(this._config!.navigation_path);
|
||||
}
|
||||
}
|
||||
|
||||
private _toggle(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const domain = (ev.currentTarget as any).domain as string;
|
||||
if (TOGGLE_DOMAINS.includes(domain)) {
|
||||
this.hass.callService(
|
||||
domain,
|
||||
this._isOn(domain) ? "turn_off" : "turn_on",
|
||||
undefined,
|
||||
{
|
||||
area_id: this._config!.area,
|
||||
}
|
||||
);
|
||||
}
|
||||
forwardHaptic("light");
|
||||
}
|
||||
|
||||
getGridOptions(): LovelaceGridOptions {
|
||||
return {
|
||||
columns: 12,
|
||||
rows: 3,
|
||||
min_columns: 3,
|
||||
};
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
height: 100%;
|
||||
:host {
|
||||
--tile-color: var(--state-icon-color);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.container {
|
||||
ha-card:has(.background:focus-visible) {
|
||||
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||
--shadow-focus: 0 0 0 1px var(--tile-color);
|
||||
border-color: var(--tile-color);
|
||||
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||
}
|
||||
ha-card {
|
||||
--ha-ripple-color: var(--tile-color);
|
||||
--ha-ripple-hover-opacity: 0.04;
|
||||
--ha-ripple-pressed-opacity: 0.12;
|
||||
height: 100%;
|
||||
transition:
|
||||
box-shadow 180ms ease-in-out,
|
||||
border-color 180ms ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(
|
||||
0,
|
||||
rgba(33, 33, 33, 0.9) 0%,
|
||||
rgba(33, 33, 33, 0) 45%
|
||||
);
|
||||
}
|
||||
|
||||
ha-card:not(.image) .container::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--sidebar-selected-icon-color);
|
||||
opacity: 0.12;
|
||||
}
|
||||
|
||||
.image hui-image {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-container ha-icon {
|
||||
--mdc-icon-size: 60px;
|
||||
color: var(--sidebar-selected-icon-color);
|
||||
}
|
||||
|
||||
.sensors {
|
||||
color: #e3e3e3;
|
||||
font-size: var(--ha-font-size-l);
|
||||
--mdc-icon-size: 24px;
|
||||
opacity: 0.6;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.sensor {
|
||||
white-space: nowrap;
|
||||
float: left;
|
||||
margin-right: 4px;
|
||||
margin-inline-end: 4px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
|
||||
.alerts {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
ha-state-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.alerts ha-state-icon {
|
||||
background: var(--accent-color);
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
padding: 8px;
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: white;
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.navigate {
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
[role="button"]:focus {
|
||||
outline: none;
|
||||
}
|
||||
.background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
margin: calc(-1 * var(--ha-card-border-width, 1px));
|
||||
overflow: hidden;
|
||||
}
|
||||
.container {
|
||||
margin: calc(-1 * var(--ha-card-border-width, 1px));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
.container.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
color: white;
|
||||
background-color: var(--area-button-color, #727272b2);
|
||||
border-radius: 50%;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
--mdc-icon-button-size: 44px;
|
||||
.content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
gap: 10px;
|
||||
}
|
||||
.on {
|
||||
color: var(--state-light-active-color);
|
||||
|
||||
.vertical {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.vertical ha-tile-info {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
}
|
||||
ha-tile-icon {
|
||||
--tile-icon-color: var(--tile-color);
|
||||
position: relative;
|
||||
padding: 6px;
|
||||
margin: -6px;
|
||||
}
|
||||
ha-tile-badge {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
inset-inline-end: 3px;
|
||||
inset-inline-start: initial;
|
||||
}
|
||||
ha-tile-info {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
transition: background-color 180ms ease-in-out;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
hui-card-features {
|
||||
--feature-color: var(--tile-color);
|
||||
padding: 0 12px 12px 12px;
|
||||
}
|
||||
.container.horizontal hui-card-features {
|
||||
width: calc(50% - var(--column-gap, 0px) / 2 - 12px);
|
||||
flex: none;
|
||||
--feature-height: 36px;
|
||||
padding: 0 12px;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -101,11 +101,13 @@ export interface EntitiesCardConfig extends LovelaceCardConfig {
|
||||
}
|
||||
|
||||
export interface AreaCardConfig extends LovelaceCardConfig {
|
||||
area: string;
|
||||
area?: string;
|
||||
navigation_path?: string;
|
||||
show_camera?: boolean;
|
||||
camera_view?: HuiImage["cameraView"];
|
||||
aspect_ratio?: string;
|
||||
sensor_classes?: string[];
|
||||
alert_classes?: string[];
|
||||
}
|
||||
|
||||
export interface ButtonCardConfig extends LovelaceCardConfig {
|
||||
|
Loading…
x
Reference in New Issue
Block a user