import type { PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, query, queryAll, state } from "lit/decorators"; import { DOMAINS_TOGGLE } from "../../../common/const"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { computeDomain } from "../../../common/entity/compute_domain"; import "../../../components/ha-card"; import type { HomeAssistant } from "../../../types"; import { computeCardSize } from "../common/compute-card-size"; import { findEntities } from "../common/find-entities"; import { applyDefaultColor } from "../common/entity-color-config"; import { processConfigEntities } from "../common/process-config-entities"; import "../components/hui-entities-toggle"; import { createHeaderFooterElement } from "../create-element/create-header-footer-element"; import { createRowElement } from "../create-element/create-row-element"; import type { EntityConfig, LovelaceRow, LovelaceRowConfig, } from "../entity-rows/types"; import type { LovelaceCard, LovelaceCardEditor, LovelaceGridOptions, LovelaceHeaderFooter, } from "../types"; import { migrateEntitiesCardConfig } from "./migrate-card-config"; import type { EntitiesCardConfig, EntitiesCardEntityConfig } from "./types"; import { haStyleScrollbar } from "../../../resources/styles"; export const computeShowHeaderToggle = < T extends EntityConfig | LovelaceRowConfig, >( config: EntitiesCardConfig, entities: T[] ): boolean => { if (config.title !== undefined && config.show_header_toggle === undefined) { // Default value is show toggle if we can at least toggle 2 entities. let toggleable = 0; for (const rowConf of entities) { if (!("entity" in rowConf)) { continue; } toggleable += Number(DOMAINS_TOGGLE.has(computeDomain(rowConf.entity))); if (toggleable === 2) { break; } } return toggleable === 2; } return !!config.show_header_toggle; }; export { migrateEntitiesCardConfig }; @customElement("hui-entities-card") class HuiEntitiesCard extends LitElement implements LovelaceCard { public static async getConfigElement(): Promise { await import("../editor/config-elements/hui-entities-card-editor"); return document.createElement("hui-entities-card-editor"); } public static getStubConfig( hass: HomeAssistant, entities: string[], entitiesFallback: string[] ): EntitiesCardConfig { const maxEntities = 3; const foundEntities = findEntities( hass, maxEntities, entities, entitiesFallback, ["light", "switch", "sensor"] ); return { type: "entities", entities: foundEntities }; } @state() private _config?: EntitiesCardConfig; @queryAll("#states > div > *") private _rowElements!: NodeListOf; @query("hui-entities-toggle") private _entitiesToggle?: HTMLElement; private _hass?: HomeAssistant; private _configEntities?: LovelaceRowConfig[]; private _showHeaderToggle?: boolean; private _headerElement?: LovelaceHeaderFooter; private _footerElement?: LovelaceHeaderFooter; connectedCallback(): void { super.connectedCallback(); this.addEventListener("row-visibility-changed", (ev) => this._updateRowVisibility(ev) ); } disconnectedCallback(): void { super.disconnectedCallback(); this.removeEventListener( "row-visibility-changed", this._updateRowVisibility ); } set hass(hass: HomeAssistant) { this._hass = hass; this._rowElements.forEach((element: unknown) => { (element as LovelaceRow).hass = hass; }); if (this._headerElement) { this._headerElement.hass = hass; } if (this._footerElement) { this._footerElement.hass = hass; } if (this._entitiesToggle) { (this._entitiesToggle as any).hass = hass; } } public async getCardSize(): Promise { if (!this._config) { return 0; } // +1 for the header let size = (this._config.title || this._showHeaderToggle ? 2 : 0) + (this._config.entities.length || 1); if (this._headerElement) { const headerSize = computeCardSize(this._headerElement); size += headerSize instanceof Promise ? await headerSize : headerSize; } if (this._footerElement) { const footerSize = computeCardSize(this._footerElement); size += footerSize instanceof Promise ? await footerSize : footerSize; } return size; } public getGridOptions(): LovelaceGridOptions { return { columns: 12, rows: "auto", min_columns: 3, }; } public setConfig(config: EntitiesCardConfig): void { if (!config.entities || !Array.isArray(config.entities)) { throw new Error("Entities must be specified"); } const migratedConfig = migrateEntitiesCardConfig(config); const entities = processConfigEntities(migratedConfig.entities); this._config = { color: "state", ...migratedConfig }; this._configEntities = entities; this._showHeaderToggle = computeShowHeaderToggle(migratedConfig, entities); if (this._config.header) { this._headerElement = createHeaderFooterElement( this._config.header ) as LovelaceHeaderFooter; this._headerElement.type = "header"; if (this._hass) { this._headerElement.hass = this._hass; } } else { this._headerElement = undefined; } if (this._config.footer) { this._footerElement = createHeaderFooterElement( this._config.footer ) as LovelaceHeaderFooter; this._footerElement.type = "footer"; if (this._hass) { this._footerElement.hass = this._hass; } } else { this._footerElement = undefined; } } 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 EntitiesCardConfig | 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); } } protected render() { if (!this._config || !this._hass) { return nothing; } return html` ${ this._headerElement ? html`` : "" } ${ !this._config.title && !this._showHeaderToggle && !this._config.icon ? "" : html`

${ this._config.icon ? html` ` : "" } ${this._config.title}
${ !this._showHeaderToggle ? nothing : html` "entity" in conf ) as EntityConfig[] ).map((conf) => conf.entity)} > ` }

` }
${this._configEntities!.map((entityConf) => this._renderEntity(entityConf) )}
${ this._footerElement ? html`` : "" }
`; } static styles = [ haStyleScrollbar, css` ha-card { height: 100%; display: flex; flex-direction: column; justify-content: space-between; } .card-header { display: flex; justify-content: space-between; } .card-header .name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #states { flex: 1; min-height: 0; overflow-x: hidden; display: flex; flex-direction: column; gap: var(--entities-card-row-gap, var(--card-row-gap, 8px)); } #states > div > * { overflow: clip visible; } #states > div { position: relative; } .icon { padding: 0px 18px 0px 8px; } .header { border-top-left-radius: var( --ha-card-border-radius, var(--ha-border-radius-lg) ); border-top-right-radius: var( --ha-card-border-radius, var(--ha-border-radius-lg) ); overflow: hidden; } .header:not(:has(> hui-buttons-header-footer)) { margin-bottom: var(--ha-space-4); } .footer { border-bottom-left-radius: var( --ha-card-border-radius, var(--ha-border-radius-lg) ); border-bottom-right-radius: var( --ha-card-border-radius, var(--ha-border-radius-lg) ); margin-top: -16px; overflow: hidden; } `, ]; private _rowConfig(entityConf: LovelaceRowConfig): LovelaceRowConfig { if (entityConf.type === "perform-action") { return { ...entityConf, type: "call-service" }; } return applyDefaultColor( entityConf as EntitiesCardEntityConfig, this._config!.color ); } private _renderEntity(entityConf: LovelaceRowConfig): TemplateResult { const element = createRowElement(this._rowConfig(entityConf)); if (this._hass) { element.hass = this._hass; } return html`
${element}
`; } private _updateRowVisibility = (ev) => { if (ev.detail?.value === false) { ev.detail?.row?.parentElement!.style.setProperty("display", "none"); } else { ev.detail?.row?.parentElement!.style.setProperty("display", ""); } }; } declare global { interface HTMLElementTagNameMap { "hui-entities-card": HuiEntitiesCard; } }