mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-11-04 08:29:52 +00:00 
			
		
		
		
	Compare commits
	
		
			35 Commits
		
	
	
		
			20251029.0
			...
			picker-lab
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					36f5e50ccc | ||
| 
						 | 
					896d76b218 | ||
| 
						 | 
					cec24117dc | ||
| 
						 | 
					34006d268b | ||
| 
						 | 
					54c03d91df | ||
| 
						 | 
					52a56a1c4e | ||
| 
						 | 
					e49feeb4aa | ||
| 
						 | 
					a0c30e433a | ||
| 
						 | 
					354ce027eb | ||
| 
						 | 
					5c224a942d | ||
| 
						 | 
					0efa4f81d4 | ||
| 
						 | 
					3ad2f35f29 | ||
| 
						 | 
					7a21d5f7bc | ||
| 
						 | 
					33226587e6 | ||
| 
						 | 
					bd2673f311 | ||
| 
						 | 
					cecadde497 | ||
| 
						 | 
					494b8811d0 | ||
| 
						 | 
					4e0a49b3da | ||
| 
						 | 
					3145fed5dc | ||
| 
						 | 
					3dd040fdc7 | ||
| 
						 | 
					e3abe9736c | ||
| 
						 | 
					fe41e72774 | ||
| 
						 | 
					7078ef52d4 | ||
| 
						 | 
					f1c9802ee3 | ||
| 
						 | 
					35697e3f94 | ||
| 
						 | 
					8ea7ad3026 | ||
| 
						 | 
					73747fbedc | ||
| 
						 | 
					aaa92bd354 | ||
| 
						 | 
					5f75fc5bcb | ||
| 
						 | 
					5fa44548c3 | ||
| 
						 | 
					1945c11621 | ||
| 
						 | 
					930575d292 | ||
| 
						 | 
					0147dbab00 | ||
| 
						 | 
					13ace24b83 | ||
| 
						 | 
					616333591a | 
							
								
								
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							@@ -36,14 +36,14 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      # Initializes the CodeQL tools for scanning.
 | 
			
		||||
      - name: Initialize CodeQL
 | 
			
		||||
        uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
 | 
			
		||||
        uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
 | 
			
		||||
        with:
 | 
			
		||||
          languages: ${{ matrix.language }}
 | 
			
		||||
 | 
			
		||||
      # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
 | 
			
		||||
      # If this step fails, then you should remove it and run the build manually (see below)
 | 
			
		||||
      - name: Autobuild
 | 
			
		||||
        uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
 | 
			
		||||
        uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
 | 
			
		||||
 | 
			
		||||
      # ℹ️ Command-line programs to run using the OS shell.
 | 
			
		||||
      # 📚 https://git.io/JvXDl
 | 
			
		||||
@@ -57,4 +57,4 @@ jobs:
 | 
			
		||||
      #   make release
 | 
			
		||||
 | 
			
		||||
      - name: Perform CodeQL Analysis
 | 
			
		||||
        uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
 | 
			
		||||
        uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
 | 
			
		||||
 
 | 
			
		||||
@@ -16,9 +16,9 @@ import {
 | 
			
		||||
} from "../../../../src/common/auth/token_storage";
 | 
			
		||||
import { atLeastVersion } from "../../../../src/common/config/version";
 | 
			
		||||
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
 | 
			
		||||
import "../../../../src/components/ha-button";
 | 
			
		||||
import "../../../../src/components/ha-icon";
 | 
			
		||||
import "../../../../src/components/ha-list";
 | 
			
		||||
import "../../../../src/components/ha-button";
 | 
			
		||||
import "../../../../src/components/ha-list-item";
 | 
			
		||||
import "../../../../src/components/ha-svg-icon";
 | 
			
		||||
import {
 | 
			
		||||
@@ -28,7 +28,6 @@ import {
 | 
			
		||||
import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types";
 | 
			
		||||
import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
 | 
			
		||||
import "../../../../src/layouts/hass-loading-screen";
 | 
			
		||||
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
 | 
			
		||||
import "./hc-layout";
 | 
			
		||||
 | 
			
		||||
@customElement("hc-cast")
 | 
			
		||||
@@ -96,7 +95,9 @@ class HcCast extends LitElement {
 | 
			
		||||
                <ha-list @action=${this._handlePickView} activatable>
 | 
			
		||||
                  ${(
 | 
			
		||||
                    this.lovelaceViews ?? [
 | 
			
		||||
                      generateDefaultViewConfig({}, {}, {}, {}, () => ""),
 | 
			
		||||
                      {
 | 
			
		||||
                        title: "Home",
 | 
			
		||||
                      },
 | 
			
		||||
                    ]
 | 
			
		||||
                  ).map(
 | 
			
		||||
                    (view, idx) => html`
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,7 @@ const SENSOR_DEVICE_CLASSES = [
 | 
			
		||||
  "pm1",
 | 
			
		||||
  "pm10",
 | 
			
		||||
  "pm25",
 | 
			
		||||
  "pm4",
 | 
			
		||||
  "power_factor",
 | 
			
		||||
  "power",
 | 
			
		||||
  "precipitation",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								package.json
									
									
									
									
									
								
							@@ -81,7 +81,7 @@
 | 
			
		||||
    "@material/mwc-top-app-bar": "0.27.0",
 | 
			
		||||
    "@material/mwc-top-app-bar-fixed": "0.27.0",
 | 
			
		||||
    "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
 | 
			
		||||
    "@material/web": "2.4.0",
 | 
			
		||||
    "@material/web": "2.4.1",
 | 
			
		||||
    "@mdi/js": "7.4.47",
 | 
			
		||||
    "@mdi/svg": "7.4.47",
 | 
			
		||||
    "@replit/codemirror-indentation-markers": "6.5.3",
 | 
			
		||||
@@ -89,8 +89,8 @@
 | 
			
		||||
    "@thomasloven/round-slider": "0.6.0",
 | 
			
		||||
    "@tsparticles/engine": "3.9.1",
 | 
			
		||||
    "@tsparticles/preset-links": "3.2.0",
 | 
			
		||||
    "@vaadin/combo-box": "24.9.2",
 | 
			
		||||
    "@vaadin/vaadin-themable-mixin": "24.9.2",
 | 
			
		||||
    "@vaadin/combo-box": "24.9.4",
 | 
			
		||||
    "@vaadin/vaadin-themable-mixin": "24.9.4",
 | 
			
		||||
    "@vibrant/color": "4.0.0",
 | 
			
		||||
    "@vue/web-component-wrapper": "1.3.0",
 | 
			
		||||
    "@webcomponents/scoped-custom-element-registry": "0.0.10",
 | 
			
		||||
@@ -154,11 +154,11 @@
 | 
			
		||||
    "@babel/preset-env": "7.28.5",
 | 
			
		||||
    "@bundle-stats/plugin-webpack-filter": "4.21.5",
 | 
			
		||||
    "@lokalise/node-api": "15.3.1",
 | 
			
		||||
    "@octokit/auth-oauth-device": "8.0.2",
 | 
			
		||||
    "@octokit/plugin-retry": "8.0.2",
 | 
			
		||||
    "@octokit/auth-oauth-device": "8.0.3",
 | 
			
		||||
    "@octokit/plugin-retry": "8.0.3",
 | 
			
		||||
    "@octokit/rest": "22.0.0",
 | 
			
		||||
    "@rsdoctor/rspack-plugin": "1.3.4",
 | 
			
		||||
    "@rspack/core": "1.5.8",
 | 
			
		||||
    "@rsdoctor/rspack-plugin": "1.3.7",
 | 
			
		||||
    "@rspack/core": "1.6.0",
 | 
			
		||||
    "@rspack/dev-server": "1.1.4",
 | 
			
		||||
    "@types/babel__plugin-transform-runtime": "7.9.5",
 | 
			
		||||
    "@types/chromecast-caf-receiver": "6.0.22",
 | 
			
		||||
@@ -178,7 +178,7 @@
 | 
			
		||||
    "@types/tar": "6.1.13",
 | 
			
		||||
    "@types/ua-parser-js": "0.7.39",
 | 
			
		||||
    "@types/webspeechapi": "0.0.29",
 | 
			
		||||
    "@vitest/coverage-v8": "4.0.3",
 | 
			
		||||
    "@vitest/coverage-v8": "4.0.5",
 | 
			
		||||
    "babel-loader": "10.0.0",
 | 
			
		||||
    "babel-plugin-template-html-minifier": "4.1.0",
 | 
			
		||||
    "browserslist-useragent-regexp": "4.1.3",
 | 
			
		||||
@@ -201,7 +201,7 @@
 | 
			
		||||
    "gulp-rename": "2.1.0",
 | 
			
		||||
    "html-minifier-terser": "7.2.0",
 | 
			
		||||
    "husky": "9.1.7",
 | 
			
		||||
    "jsdom": "27.0.1",
 | 
			
		||||
    "jsdom": "27.1.0",
 | 
			
		||||
    "jszip": "3.10.1",
 | 
			
		||||
    "lint-staged": "16.2.6",
 | 
			
		||||
    "lit-analyzer": "2.0.3",
 | 
			
		||||
@@ -213,13 +213,13 @@
 | 
			
		||||
    "rspack-manifest-plugin": "5.1.0",
 | 
			
		||||
    "serve": "14.2.5",
 | 
			
		||||
    "sinon": "21.0.0",
 | 
			
		||||
    "tar": "7.5.1",
 | 
			
		||||
    "tar": "7.5.2",
 | 
			
		||||
    "terser-webpack-plugin": "5.3.14",
 | 
			
		||||
    "ts-lit-plugin": "2.0.2",
 | 
			
		||||
    "typescript": "5.9.3",
 | 
			
		||||
    "typescript-eslint": "8.46.2",
 | 
			
		||||
    "vite-tsconfig-paths": "5.1.4",
 | 
			
		||||
    "vitest": "4.0.3",
 | 
			
		||||
    "vitest": "4.0.5",
 | 
			
		||||
    "webpack-stats-plugin": "1.1.3",
 | 
			
		||||
    "webpackbar": "7.0.0",
 | 
			
		||||
    "workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
 | 
			
		||||
 
 | 
			
		||||
@@ -214,6 +214,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
 | 
			
		||||
      "pm1",
 | 
			
		||||
      "pm10",
 | 
			
		||||
      "pm25",
 | 
			
		||||
      "pm4",
 | 
			
		||||
      "power_factor",
 | 
			
		||||
      "power",
 | 
			
		||||
      "pressure",
 | 
			
		||||
 
 | 
			
		||||
@@ -147,7 +147,7 @@ class HaEntitiesPicker extends LitElement {
 | 
			
		||||
          .createDomains=${this.createDomains}
 | 
			
		||||
          .required=${this.required && !currentEntities.length}
 | 
			
		||||
          @value-changed=${this._addEntity}
 | 
			
		||||
          add-button
 | 
			
		||||
          .addButton=${currentEntities.length > 0}
 | 
			
		||||
        ></ha-entity-picker>
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
 
 | 
			
		||||
@@ -357,7 +357,11 @@ export class HaAreaPicker extends LitElement {
 | 
			
		||||
 | 
			
		||||
  protected render(): TemplateResult {
 | 
			
		||||
    const placeholder =
 | 
			
		||||
      this.placeholder ?? this.hass.localize("ui.components.area-picker.area");
 | 
			
		||||
      this.placeholder ??
 | 
			
		||||
      this.hass.localize("ui.components.area-picker.placeholder");
 | 
			
		||||
    const notFoundLabel = this.hass.localize(
 | 
			
		||||
      "ui.components.area-picker.no_match"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const valueRenderer = this._computeValueRenderer(this.hass.areas);
 | 
			
		||||
 | 
			
		||||
@@ -367,9 +371,7 @@ export class HaAreaPicker extends LitElement {
 | 
			
		||||
        .autofocus=${this.autofocus}
 | 
			
		||||
        .label=${this.label}
 | 
			
		||||
        .helper=${this.helper}
 | 
			
		||||
        .notFoundLabel=${this.hass.localize(
 | 
			
		||||
          "ui.components.area-picker.no_match"
 | 
			
		||||
        )}
 | 
			
		||||
        .notFoundLabel=${notFoundLabel}
 | 
			
		||||
        .placeholder=${placeholder}
 | 
			
		||||
        .value=${this.value}
 | 
			
		||||
        .getItems=${this._getItems}
 | 
			
		||||
 
 | 
			
		||||
@@ -59,6 +59,7 @@ export class HaButton extends Button {
 | 
			
		||||
          line-height: 1;
 | 
			
		||||
 | 
			
		||||
          transition: background-color 0.15s ease-in-out;
 | 
			
		||||
          text-wrap: wrap;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host([size="small"]) .button {
 | 
			
		||||
 
 | 
			
		||||
@@ -148,7 +148,7 @@ export class HaForm extends LitElement implements HaFormElement {
 | 
			
		||||
                  .value=${getValue(this.data, item)}
 | 
			
		||||
                  .label=${this._computeLabel(item, this.data)}
 | 
			
		||||
                  .disabled=${item.disabled || this.disabled || false}
 | 
			
		||||
                  .placeholder=${item.required ? "" : item.default}
 | 
			
		||||
                  .placeholder=${item.required ? undefined : item.default}
 | 
			
		||||
                  .helper=${this._computeHelper(item)}
 | 
			
		||||
                  .localizeValue=${this.localizeValue}
 | 
			
		||||
                  .required=${item.required || false}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@ import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
 | 
			
		||||
import { mdiPlaylistPlus } from "@mdi/js";
 | 
			
		||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
 | 
			
		||||
import { customElement, property, query, state } from "lit/decorators";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined";
 | 
			
		||||
import { tinykeys } from "tinykeys";
 | 
			
		||||
import { fireEvent } from "../common/dom/fire_event";
 | 
			
		||||
import type { HomeAssistant } from "../types";
 | 
			
		||||
@@ -107,9 +106,6 @@ export class HaGenericPicker extends LitElement {
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    return html`
 | 
			
		||||
      ${this.label
 | 
			
		||||
        ? html`<label ?disabled=${this.disabled}>${this.label}</label>`
 | 
			
		||||
        : nothing}
 | 
			
		||||
      <div class="container">
 | 
			
		||||
        <div id="picker">
 | 
			
		||||
          <slot name="field">
 | 
			
		||||
@@ -130,7 +126,7 @@ export class HaGenericPicker extends LitElement {
 | 
			
		||||
                  type="button"
 | 
			
		||||
                  class=${this._opened ? "opened" : ""}
 | 
			
		||||
                  compact
 | 
			
		||||
                  aria-label=${ifDefined(this.label)}
 | 
			
		||||
                  .label=${this.label}
 | 
			
		||||
                  @click=${this.open}
 | 
			
		||||
                  @clear=${this._clear}
 | 
			
		||||
                  .placeholder=${this.placeholder}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import {
 | 
			
		||||
  type TemplateResult,
 | 
			
		||||
} from "lit";
 | 
			
		||||
import { customElement, property, query } from "lit/decorators";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined";
 | 
			
		||||
import { fireEvent } from "../common/dom/fire_event";
 | 
			
		||||
import "./ha-combo-box-item";
 | 
			
		||||
import type { HaComboBoxItem } from "./ha-combo-box-item";
 | 
			
		||||
@@ -33,6 +34,8 @@ export class HaPickerField extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @property() public placeholder?: string;
 | 
			
		||||
 | 
			
		||||
  @property() public label?: string;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: "hide-clear-icon", type: Boolean })
 | 
			
		||||
  public hideClearIcon = false;
 | 
			
		||||
 | 
			
		||||
@@ -51,15 +54,35 @@ export class HaPickerField extends LitElement {
 | 
			
		||||
      !!this.value && !this.required && !this.disabled && !this.hideClearIcon;
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <ha-combo-box-item .disabled=${this.disabled} type="button" compact>
 | 
			
		||||
      <ha-combo-box-item
 | 
			
		||||
        .disabled=${this.disabled}
 | 
			
		||||
        type="button"
 | 
			
		||||
        compact
 | 
			
		||||
        aria-label=${ifDefined(this.label)}
 | 
			
		||||
      >
 | 
			
		||||
        ${this.value
 | 
			
		||||
          ? this.valueRenderer
 | 
			
		||||
            ? this.valueRenderer(this.value)
 | 
			
		||||
            : html`<slot name="headline">${this.value}</slot>`
 | 
			
		||||
            : html`<span slot="headline">${this.value}</span>`
 | 
			
		||||
          : html`
 | 
			
		||||
              <span slot="headline" class="placeholder">
 | 
			
		||||
                ${this.placeholder}
 | 
			
		||||
              </span>
 | 
			
		||||
              ${this.label
 | 
			
		||||
                ? html`
 | 
			
		||||
                    <span
 | 
			
		||||
                      slot="headline"
 | 
			
		||||
                      class="label ${this.placeholder
 | 
			
		||||
                        ? "with-placeholder"
 | 
			
		||||
                        : ""}"
 | 
			
		||||
                      >${this.label}</span
 | 
			
		||||
                    >
 | 
			
		||||
                  `
 | 
			
		||||
                : nothing}
 | 
			
		||||
              ${this.placeholder
 | 
			
		||||
                ? html`
 | 
			
		||||
                    <span slot="headline" class="placeholder">
 | 
			
		||||
                      ${this.placeholder}
 | 
			
		||||
                    </span>
 | 
			
		||||
                  `
 | 
			
		||||
                : nothing}
 | 
			
		||||
            `}
 | 
			
		||||
        ${showClearIcon
 | 
			
		||||
          ? html`
 | 
			
		||||
@@ -152,9 +175,24 @@ export class HaPickerField extends LitElement {
 | 
			
		||||
          width: 32px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .label {
 | 
			
		||||
          padding: 0 8px;
 | 
			
		||||
          color: var(--secondary-text-color);
 | 
			
		||||
          line-height: var(--ha-line-height-normal);
 | 
			
		||||
          font-size: var(--ha-font-size-m);
 | 
			
		||||
          white-space: nowrap;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .label.with-placeholder {
 | 
			
		||||
          line-height: var(--ha-line-height-condensed);
 | 
			
		||||
          font-size: var(--ha-font-size-xs);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .placeholder {
 | 
			
		||||
          color: var(--secondary-text-color);
 | 
			
		||||
          padding: 0 8px;
 | 
			
		||||
          line-height: var(--ha-line-height-normal);
 | 
			
		||||
          font-size: var(--ha-font-size-m);
 | 
			
		||||
        }
 | 
			
		||||
      `,
 | 
			
		||||
    ];
 | 
			
		||||
 
 | 
			
		||||
@@ -16,14 +16,10 @@ import memoizeOne from "memoize-one";
 | 
			
		||||
import { computeCssColor } from "../../common/color/compute-color";
 | 
			
		||||
import { hex2rgb } from "../../common/color/convert-color";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import { slugify } from "../../common/string/slugify";
 | 
			
		||||
import {
 | 
			
		||||
  computeDeviceName,
 | 
			
		||||
  computeDeviceNameDisplay,
 | 
			
		||||
} from "../../common/entity/compute_device_name";
 | 
			
		||||
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
 | 
			
		||||
import { computeDomain } from "../../common/entity/compute_domain";
 | 
			
		||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
 | 
			
		||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
 | 
			
		||||
import { computeStateName } from "../../common/entity/compute_state_name";
 | 
			
		||||
import { slugify } from "../../common/string/slugify";
 | 
			
		||||
import { getConfigEntry } from "../../data/config_entries";
 | 
			
		||||
import { labelsContext } from "../../data/context";
 | 
			
		||||
import { domainToName } from "../../data/integration";
 | 
			
		||||
@@ -172,23 +168,10 @@ export class HaTargetPickerValueChip extends LitElement {
 | 
			
		||||
    if (type === "entity") {
 | 
			
		||||
      this._setDomainName(computeDomain(itemId));
 | 
			
		||||
 | 
			
		||||
      const stateObject = this.hass.states[itemId];
 | 
			
		||||
      const entityName = computeEntityName(
 | 
			
		||||
        stateObject,
 | 
			
		||||
        this.hass.entities,
 | 
			
		||||
        this.hass.devices
 | 
			
		||||
      );
 | 
			
		||||
      const { device } = getEntityContext(
 | 
			
		||||
        stateObject,
 | 
			
		||||
        this.hass.entities,
 | 
			
		||||
        this.hass.devices,
 | 
			
		||||
        this.hass.areas,
 | 
			
		||||
        this.hass.floors
 | 
			
		||||
      );
 | 
			
		||||
      const deviceName = device ? computeDeviceName(device) : undefined;
 | 
			
		||||
      const stateObj = this.hass.states[itemId];
 | 
			
		||||
      return {
 | 
			
		||||
        name: entityName || deviceName || itemId,
 | 
			
		||||
        stateObject,
 | 
			
		||||
        name: computeStateName(stateObj) || itemId,
 | 
			
		||||
        stateObject: stateObj,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -152,10 +152,18 @@ export class MoreInfoHistory extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _setRedrawTimer() {
 | 
			
		||||
    // redraw the graph every minute to update the time axis
 | 
			
		||||
  private _setUpdateTimer() {
 | 
			
		||||
    clearInterval(this._interval);
 | 
			
		||||
    this._interval = window.setInterval(() => this._redrawGraph(), 1000 * 60);
 | 
			
		||||
    this._interval = window.setInterval(() => {
 | 
			
		||||
      // If using statistics, refresh the data
 | 
			
		||||
      if (this._statistics) {
 | 
			
		||||
        this._fetchStatistics();
 | 
			
		||||
      }
 | 
			
		||||
      // If using history, redraw the graph to update the time axis
 | 
			
		||||
      if (this._stateHistory) {
 | 
			
		||||
        this._redrawGraph();
 | 
			
		||||
      }
 | 
			
		||||
    }, 1000 * 60);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _getStatisticsMetaData(statisticIds: string[] | undefined) {
 | 
			
		||||
@@ -170,6 +178,30 @@ export class MoreInfoHistory extends LitElement {
 | 
			
		||||
    return statisticsMetaData;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _fetchStatistics(): Promise<boolean> {
 | 
			
		||||
    // Fire off the metadata and fetch at the same time
 | 
			
		||||
    // to avoid waiting in sequence so the UI responds
 | 
			
		||||
    // faster.
 | 
			
		||||
    const _metadata = this._getStatisticsMetaData([this.entityId]);
 | 
			
		||||
    const _statistics = fetchStatistics(
 | 
			
		||||
      this.hass!,
 | 
			
		||||
      subHours(new Date(), 24),
 | 
			
		||||
      undefined,
 | 
			
		||||
      [this.entityId],
 | 
			
		||||
      "5minute",
 | 
			
		||||
      undefined,
 | 
			
		||||
      statTypes
 | 
			
		||||
    );
 | 
			
		||||
    const [metadata, statistics] = await Promise.all([_metadata, _statistics]);
 | 
			
		||||
    if (metadata && Object.keys(metadata).length) {
 | 
			
		||||
      this._metadata = metadata;
 | 
			
		||||
      this._statistics = statistics;
 | 
			
		||||
      this._statNames = { [this.entityId]: "" };
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _getStateHistory(): Promise<void> {
 | 
			
		||||
    if (
 | 
			
		||||
      isComponentLoaded(this.hass, "recorder") &&
 | 
			
		||||
@@ -180,27 +212,10 @@ export class MoreInfoHistory extends LitElement {
 | 
			
		||||
      // has not opted into statistics so there is no need to check as it
 | 
			
		||||
      // requires another round-trip to the server.
 | 
			
		||||
      if (stateObj && stateObj.attributes.state_class) {
 | 
			
		||||
        // Fire off the metadata and fetch at the same time
 | 
			
		||||
        // to avoid waiting in sequence so the UI responds
 | 
			
		||||
        // faster.
 | 
			
		||||
        const _metadata = this._getStatisticsMetaData([this.entityId]);
 | 
			
		||||
        const _statistics = fetchStatistics(
 | 
			
		||||
          this.hass!,
 | 
			
		||||
          subHours(new Date(), 24),
 | 
			
		||||
          undefined,
 | 
			
		||||
          [this.entityId],
 | 
			
		||||
          "5minute",
 | 
			
		||||
          undefined,
 | 
			
		||||
          statTypes
 | 
			
		||||
        );
 | 
			
		||||
        const [metadata, statistics] = await Promise.all([
 | 
			
		||||
          _metadata,
 | 
			
		||||
          _statistics,
 | 
			
		||||
        ]);
 | 
			
		||||
        if (metadata && Object.keys(metadata).length) {
 | 
			
		||||
          this._metadata = metadata;
 | 
			
		||||
          this._statistics = statistics;
 | 
			
		||||
          this._statNames = { [this.entityId]: "" };
 | 
			
		||||
        const hasStatistics = await this._fetchStatistics();
 | 
			
		||||
        if (hasStatistics) {
 | 
			
		||||
          // Using statistics, set up refresh timer
 | 
			
		||||
          this._setUpdateTimer();
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@@ -238,7 +253,7 @@ export class MoreInfoHistory extends LitElement {
 | 
			
		||||
      this._error = err;
 | 
			
		||||
      return undefined;
 | 
			
		||||
    });
 | 
			
		||||
    this._setRedrawTimer();
 | 
			
		||||
    this._setUpdateTimer();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static styles = [
 | 
			
		||||
 
 | 
			
		||||
@@ -97,6 +97,9 @@ export const ENTITY_COMPONENT_ICONS: Record<string, ComponentIcons> = {
 | 
			
		||||
    pm25: {
 | 
			
		||||
      default: "mdi:molecule",
 | 
			
		||||
    },
 | 
			
		||||
    pm4: {
 | 
			
		||||
      default: "mdi:molecule",
 | 
			
		||||
    },
 | 
			
		||||
    power: {
 | 
			
		||||
      default: "mdi:flash",
 | 
			
		||||
    },
 | 
			
		||||
@@ -674,6 +677,9 @@ export const ENTITY_COMPONENT_ICONS: Record<string, ComponentIcons> = {
 | 
			
		||||
    pm25: {
 | 
			
		||||
      default: "mdi:molecule",
 | 
			
		||||
    },
 | 
			
		||||
    pm4: {
 | 
			
		||||
      default: "mdi:molecule",
 | 
			
		||||
    },
 | 
			
		||||
    power: {
 | 
			
		||||
      default: "mdi:flash",
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -332,6 +332,15 @@ class DialogCalendarEventEditor extends LitElement {
 | 
			
		||||
 | 
			
		||||
  private _allDayToggleChanged(ev) {
 | 
			
		||||
    this._allDay = ev.target.checked;
 | 
			
		||||
    // When switching to all-day mode, normalize dates to midnight so time portions don't interfere with date comparisons
 | 
			
		||||
    if (this._allDay && this._dtstart && this._dtend) {
 | 
			
		||||
      this._dtstart = new Date(
 | 
			
		||||
        formatDate(this._dtstart, this._timeZone!) + "T00:00:00"
 | 
			
		||||
      );
 | 
			
		||||
      this._dtend = new Date(
 | 
			
		||||
        formatDate(this._dtend, this._timeZone!) + "T00:00:00"
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _startDateChanged(ev: CustomEvent) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import { consume } from "@lit/context";
 | 
			
		||||
import {
 | 
			
		||||
  mdiAppleKeyboardCommand,
 | 
			
		||||
  mdiContentCopy,
 | 
			
		||||
@@ -13,6 +14,7 @@ import {
 | 
			
		||||
import { html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property, query, state } from "lit/decorators";
 | 
			
		||||
import { keyed } from "lit/directives/keyed";
 | 
			
		||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
 | 
			
		||||
import { fireEvent } from "../../../../common/dom/fire_event";
 | 
			
		||||
import { handleStructError } from "../../../../common/structs/handle-errors";
 | 
			
		||||
import type { LocalizeKeys } from "../../../../common/translations/localize";
 | 
			
		||||
@@ -20,7 +22,16 @@ import "../../../../components/ha-md-divider";
 | 
			
		||||
import "../../../../components/ha-md-menu-item";
 | 
			
		||||
import { ACTION_BUILDING_BLOCKS } from "../../../../data/action";
 | 
			
		||||
import type { ActionSidebarConfig } from "../../../../data/automation";
 | 
			
		||||
import {
 | 
			
		||||
  floorsContext,
 | 
			
		||||
  fullEntitiesContext,
 | 
			
		||||
  labelsContext,
 | 
			
		||||
} from "../../../../data/context";
 | 
			
		||||
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
 | 
			
		||||
import type { FloorRegistryEntry } from "../../../../data/floor_registry";
 | 
			
		||||
import type { LabelRegistryEntry } from "../../../../data/label_registry";
 | 
			
		||||
import type { RepeatAction } from "../../../../data/script";
 | 
			
		||||
import { describeAction } from "../../../../data/script_i18n";
 | 
			
		||||
import type { HomeAssistant } from "../../../../types";
 | 
			
		||||
import { isMac } from "../../../../util/is_mac";
 | 
			
		||||
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
 | 
			
		||||
@@ -49,6 +60,18 @@ export default class HaAutomationSidebarAction extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @state() private _warnings?: string[];
 | 
			
		||||
 | 
			
		||||
  @state()
 | 
			
		||||
  @consume({ context: fullEntitiesContext, subscribe: true })
 | 
			
		||||
  _entityReg!: EntityRegistryEntry[];
 | 
			
		||||
 | 
			
		||||
  @state()
 | 
			
		||||
  @consume({ context: labelsContext, subscribe: true })
 | 
			
		||||
  _labelReg!: LabelRegistryEntry[];
 | 
			
		||||
 | 
			
		||||
  @state()
 | 
			
		||||
  @consume({ context: floorsContext, subscribe: true })
 | 
			
		||||
  _floorReg!: Record<string, FloorRegistryEntry>;
 | 
			
		||||
 | 
			
		||||
  @query(".sidebar-editor")
 | 
			
		||||
  public editor?: HaAutomationConditionEditor;
 | 
			
		||||
 | 
			
		||||
@@ -79,15 +102,20 @@ export default class HaAutomationSidebarAction extends LitElement {
 | 
			
		||||
 | 
			
		||||
    const isBuildingBlock = ACTION_BUILDING_BLOCKS.includes(type || "");
 | 
			
		||||
 | 
			
		||||
    const title = capitalizeFirstLetter(
 | 
			
		||||
      describeAction(
 | 
			
		||||
        this.hass,
 | 
			
		||||
        this._entityReg,
 | 
			
		||||
        this._labelReg,
 | 
			
		||||
        this._floorReg,
 | 
			
		||||
        actionConfig
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const subtitle = this.hass.localize(
 | 
			
		||||
      "ui.panel.config.automation.editor.actions.action"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const title =
 | 
			
		||||
      this.hass.localize(
 | 
			
		||||
        `ui.panel.config.automation.editor.actions.type.${type}.label` as LocalizeKeys
 | 
			
		||||
      ) || type;
 | 
			
		||||
 | 
			
		||||
    const description = isBuildingBlock
 | 
			
		||||
      ? this.hass.localize(
 | 
			
		||||
          `ui.panel.config.automation.editor.actions.type.${type}.description.picker` as LocalizeKeys
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import {
 | 
			
		||||
  mdiContentCopy,
 | 
			
		||||
  mdiContentCut,
 | 
			
		||||
  mdiDelete,
 | 
			
		||||
  mdiIdentifier,
 | 
			
		||||
  mdiPlayCircleOutline,
 | 
			
		||||
  mdiPlaylistEdit,
 | 
			
		||||
  mdiPlusCircleMultipleOutline,
 | 
			
		||||
@@ -40,6 +41,8 @@ export default class HaAutomationSidebarTrigger extends LitElement {
 | 
			
		||||
  @property({ type: Number, attribute: "sidebar-key" })
 | 
			
		||||
  public sidebarKey?: number;
 | 
			
		||||
 | 
			
		||||
  @state() private _requestShowId = false;
 | 
			
		||||
 | 
			
		||||
  @state() private _warnings?: string[];
 | 
			
		||||
 | 
			
		||||
  @query(".sidebar-editor")
 | 
			
		||||
@@ -47,6 +50,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
 | 
			
		||||
 | 
			
		||||
  protected willUpdate(changedProperties) {
 | 
			
		||||
    if (changedProperties.has("config")) {
 | 
			
		||||
      this._requestShowId = false;
 | 
			
		||||
      this._warnings = undefined;
 | 
			
		||||
      if (this.config) {
 | 
			
		||||
        this.yamlMode = this.config.yamlMode;
 | 
			
		||||
@@ -101,6 +105,24 @@ export default class HaAutomationSidebarTrigger extends LitElement {
 | 
			
		||||
          </div>
 | 
			
		||||
        </ha-md-menu-item>
 | 
			
		||||
 | 
			
		||||
        ${!this.yamlMode &&
 | 
			
		||||
        !("id" in this.config.config) &&
 | 
			
		||||
        !this._requestShowId
 | 
			
		||||
          ? html`<ha-md-menu-item
 | 
			
		||||
              slot="menu-items"
 | 
			
		||||
              .clickAction=${this._showTriggerId}
 | 
			
		||||
              .disabled=${this.disabled || type === "list"}
 | 
			
		||||
            >
 | 
			
		||||
              <ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
 | 
			
		||||
              <div class="overflow-label">
 | 
			
		||||
                ${this.hass.localize(
 | 
			
		||||
                  "ui.panel.config.automation.editor.triggers.edit_id"
 | 
			
		||||
                )}
 | 
			
		||||
                <span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
 | 
			
		||||
              </div>
 | 
			
		||||
            </ha-md-menu-item>`
 | 
			
		||||
          : nothing}
 | 
			
		||||
 | 
			
		||||
        <ha-md-divider
 | 
			
		||||
          slot="menu-items"
 | 
			
		||||
          role="separator"
 | 
			
		||||
@@ -250,6 +272,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
 | 
			
		||||
            @value-changed=${this._valueChangedSidebar}
 | 
			
		||||
            @yaml-changed=${this._yamlChangedSidebar}
 | 
			
		||||
            .uiSupported=${this.config.uiSupported}
 | 
			
		||||
            .showId=${this._requestShowId}
 | 
			
		||||
            .yamlMode=${this.yamlMode}
 | 
			
		||||
            .disabled=${this.disabled}
 | 
			
		||||
            @ui-mode-not-available=${this._handleUiModeNotAvailable}
 | 
			
		||||
@@ -292,6 +315,10 @@ export default class HaAutomationSidebarTrigger extends LitElement {
 | 
			
		||||
    fireEvent(this, "toggle-yaml-mode");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private _showTriggerId = () => {
 | 
			
		||||
    this._requestShowId = true;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static styles = [sidebarEditorStyles, overflowStyles];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,8 @@ export default class HaAutomationTriggerEditor extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, attribute: "show-id" }) public showId = false;
 | 
			
		||||
 | 
			
		||||
  @query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
@@ -36,6 +38,8 @@ export default class HaAutomationTriggerEditor extends LitElement {
 | 
			
		||||
 | 
			
		||||
    const yamlMode = this.yamlMode || !this.uiSupported;
 | 
			
		||||
 | 
			
		||||
    const showId = "id" in this.trigger || this.showId;
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <div
 | 
			
		||||
        class=${classMap({
 | 
			
		||||
@@ -70,20 +74,15 @@ export default class HaAutomationTriggerEditor extends LitElement {
 | 
			
		||||
              ></ha-yaml-editor>
 | 
			
		||||
            `
 | 
			
		||||
          : html`
 | 
			
		||||
              ${!isTriggerList(this.trigger)
 | 
			
		||||
              ${showId && !isTriggerList(this.trigger)
 | 
			
		||||
                ? html`
 | 
			
		||||
                    <ha-textfield
 | 
			
		||||
                      .label=${`${this.hass.localize(
 | 
			
		||||
                      .label=${this.hass.localize(
 | 
			
		||||
                        "ui.panel.config.automation.editor.triggers.id"
 | 
			
		||||
                      )} (${this.hass.localize(
 | 
			
		||||
                        "ui.panel.config.automation.editor.triggers.optional"
 | 
			
		||||
                      )})`}
 | 
			
		||||
                      )}
 | 
			
		||||
                      .value=${this.trigger.id || ""}
 | 
			
		||||
                      .disabled=${this.disabled}
 | 
			
		||||
                      @change=${this._idChanged}
 | 
			
		||||
                      .helper=${this.hass.localize(
 | 
			
		||||
                        "ui.panel.config.automation.editor.triggers.id_helper"
 | 
			
		||||
                      )}
 | 
			
		||||
                    ></ha-textfield>
 | 
			
		||||
                  `
 | 
			
		||||
                : nothing}
 | 
			
		||||
 
 | 
			
		||||
@@ -245,18 +245,20 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
 | 
			
		||||
      newTrigger.to,
 | 
			
		||||
      newTrigger.attribute
 | 
			
		||||
    );
 | 
			
		||||
    if (Array.isArray(newTrigger.to) && newTrigger.to.length === 0) {
 | 
			
		||||
      delete newTrigger.to;
 | 
			
		||||
    }
 | 
			
		||||
    newTrigger.from = this._applyAnyStateExclusive(
 | 
			
		||||
      newTrigger.from,
 | 
			
		||||
      newTrigger.attribute
 | 
			
		||||
    );
 | 
			
		||||
    if (Array.isArray(newTrigger.from) && newTrigger.from.length === 0) {
 | 
			
		||||
      delete newTrigger.from;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Object.keys(newTrigger).forEach((key) => {
 | 
			
		||||
      const val = newTrigger[key];
 | 
			
		||||
      if (
 | 
			
		||||
        val === undefined ||
 | 
			
		||||
        val === "" ||
 | 
			
		||||
        (Array.isArray(val) && val.length === 0)
 | 
			
		||||
      ) {
 | 
			
		||||
      if (val === undefined || val === "") {
 | 
			
		||||
        delete newTrigger[key];
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ import { subscribeEntityRegistry } from "../../../data/entity_registry";
 | 
			
		||||
import type { UpdateEntity } from "../../../data/update";
 | 
			
		||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import "../../../components/ha-progress-ring";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-config-updates")
 | 
			
		||||
class HaConfigUpdates extends SubscribeMixin(LitElement) {
 | 
			
		||||
@@ -56,6 +57,29 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
 | 
			
		||||
      this._entities?.find((entity) => entity.entity_id === entityId)
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  private _renderUpdateProgress(entity: UpdateEntity) {
 | 
			
		||||
    if (entity.attributes.update_percentage != null) {
 | 
			
		||||
      return html`<ha-progress-ring
 | 
			
		||||
        size="small"
 | 
			
		||||
        .value=${entity.attributes.update_percentage}
 | 
			
		||||
        .label=${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.updates.update_in_progress"
 | 
			
		||||
        )}
 | 
			
		||||
      ></ha-progress-ring>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (entity.attributes.in_progress) {
 | 
			
		||||
      return html`<ha-spinner
 | 
			
		||||
        size="small"
 | 
			
		||||
        .ariaLabel=${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.updates.update_in_progress"
 | 
			
		||||
        )}
 | 
			
		||||
      ></ha-spinner>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return html`<ha-icon-next></ha-icon-next>`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    if (!this.updateEntities?.length) {
 | 
			
		||||
      return nothing;
 | 
			
		||||
@@ -106,13 +130,9 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
 | 
			
		||||
                  )}
 | 
			
		||||
                ></state-badge>
 | 
			
		||||
                ${this.narrow && entity.attributes.in_progress
 | 
			
		||||
                  ? html`<ha-spinner
 | 
			
		||||
                      class="absolute"
 | 
			
		||||
                      size="small"
 | 
			
		||||
                      .ariaLabel=${this.hass.localize(
 | 
			
		||||
                        "ui.panel.config.updates.update_in_progress"
 | 
			
		||||
                      )}
 | 
			
		||||
                    ></ha-spinner>`
 | 
			
		||||
                  ? html`<div class="absolute">
 | 
			
		||||
                      ${this._renderUpdateProgress(entity)}
 | 
			
		||||
                    </div>`
 | 
			
		||||
                  : nothing}
 | 
			
		||||
              </div>
 | 
			
		||||
              <span slot="headline"
 | 
			
		||||
@@ -128,16 +148,9 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
 | 
			
		||||
                  : nothing}
 | 
			
		||||
              </span>
 | 
			
		||||
              ${!this.narrow
 | 
			
		||||
                ? entity.attributes.in_progress
 | 
			
		||||
                  ? html`<div slot="end">
 | 
			
		||||
                      <ha-spinner
 | 
			
		||||
                        size="small"
 | 
			
		||||
                        .ariaLabel=${this.hass.localize(
 | 
			
		||||
                          "ui.panel.config.updates.update_in_progress"
 | 
			
		||||
                        )}
 | 
			
		||||
                      ></ha-spinner>
 | 
			
		||||
                    </div>`
 | 
			
		||||
                  : html`<ha-icon-next slot="end"></ha-icon-next>`
 | 
			
		||||
                ? html`<div slot="end">
 | 
			
		||||
                    ${this._renderUpdateProgress(entity)}
 | 
			
		||||
                  </div>`
 | 
			
		||||
                : nothing}
 | 
			
		||||
            </ha-md-list-item>
 | 
			
		||||
          `;
 | 
			
		||||
@@ -193,13 +206,13 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
 | 
			
		||||
        div[slot="start"] {
 | 
			
		||||
          position: relative;
 | 
			
		||||
        }
 | 
			
		||||
        ha-spinner.absolute {
 | 
			
		||||
        div.absolute {
 | 
			
		||||
          position: absolute;
 | 
			
		||||
          left: 6px;
 | 
			
		||||
          top: 6px;
 | 
			
		||||
        }
 | 
			
		||||
        state-badge.updating {
 | 
			
		||||
          opacity: 0.5;
 | 
			
		||||
          opacity: 0.2;
 | 
			
		||||
        }
 | 
			
		||||
      `,
 | 
			
		||||
    ];
 | 
			
		||||
 
 | 
			
		||||
@@ -228,7 +228,7 @@ export class HaDeviceEntitiesCard extends LitElement {
 | 
			
		||||
    addEntitiesToLovelaceView(
 | 
			
		||||
      this,
 | 
			
		||||
      this.hass,
 | 
			
		||||
      computeCards(this.hass.states, entities, {
 | 
			
		||||
      computeCards(this.hass, entities, {
 | 
			
		||||
        title: this.deviceName,
 | 
			
		||||
      }),
 | 
			
		||||
      computeSection(entities, {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,7 @@ import { fireEvent } from "../../../../common/dom/fire_event";
 | 
			
		||||
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
 | 
			
		||||
import "../../../../components/ha-alert";
 | 
			
		||||
import "../../../../components/ha-area-picker";
 | 
			
		||||
import "../../../../components/ha-wa-dialog";
 | 
			
		||||
import "../../../../components/ha-dialog-footer";
 | 
			
		||||
import "../../../../components/ha-dialog";
 | 
			
		||||
import "../../../../components/ha-button";
 | 
			
		||||
import "../../../../components/ha-labels-picker";
 | 
			
		||||
import type { HaSwitch } from "../../../../components/ha-switch";
 | 
			
		||||
@@ -20,8 +19,6 @@ import type { DeviceRegistryDetailDialogParams } from "./show-dialog-device-regi
 | 
			
		||||
class DialogDeviceRegistryDetail extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @state() private _open = false;
 | 
			
		||||
 | 
			
		||||
  @state() private _nameByUser!: string;
 | 
			
		||||
 | 
			
		||||
  @state() private _error?: string;
 | 
			
		||||
@@ -45,15 +42,10 @@ class DialogDeviceRegistryDetail extends LitElement {
 | 
			
		||||
    this._areaId = this._params.device.area_id || "";
 | 
			
		||||
    this._labels = this._params.device.labels || [];
 | 
			
		||||
    this._disabledBy = this._params.device.disabled_by;
 | 
			
		||||
    this._open = true;
 | 
			
		||||
    await this.updateComplete;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public closeDialog(): void {
 | 
			
		||||
    this._open = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _dialogClosed(): void {
 | 
			
		||||
    this._error = "";
 | 
			
		||||
    this._params = undefined;
 | 
			
		||||
    fireEvent(this, "dialog-closed", { dialog: this.localName });
 | 
			
		||||
@@ -65,12 +57,10 @@ class DialogDeviceRegistryDetail extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
    const device = this._params.device;
 | 
			
		||||
    return html`
 | 
			
		||||
      <ha-wa-dialog
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        .open=${this._open}
 | 
			
		||||
        header-title=${computeDeviceNameDisplay(device, this.hass)}
 | 
			
		||||
        prevent-scrim-close
 | 
			
		||||
        @closed=${this._dialogClosed}
 | 
			
		||||
      <ha-dialog
 | 
			
		||||
        open
 | 
			
		||||
        @closed=${this.closeDialog}
 | 
			
		||||
        .heading=${computeDeviceNameDisplay(device, this.hass)}
 | 
			
		||||
      >
 | 
			
		||||
        <div>
 | 
			
		||||
          ${this._error
 | 
			
		||||
@@ -78,7 +68,6 @@ class DialogDeviceRegistryDetail extends LitElement {
 | 
			
		||||
            : ""}
 | 
			
		||||
          <div class="form">
 | 
			
		||||
            <ha-textfield
 | 
			
		||||
              autofocus
 | 
			
		||||
              .value=${this._nameByUser}
 | 
			
		||||
              @input=${this._nameChanged}
 | 
			
		||||
              .label=${this.hass.localize(
 | 
			
		||||
@@ -86,6 +75,7 @@ class DialogDeviceRegistryDetail extends LitElement {
 | 
			
		||||
              )}
 | 
			
		||||
              .placeholder=${device.name || ""}
 | 
			
		||||
              .disabled=${this._submitting}
 | 
			
		||||
              dialogInitialFocus
 | 
			
		||||
            ></ha-textfield>
 | 
			
		||||
            <ha-area-picker
 | 
			
		||||
              .hass=${this.hass}
 | 
			
		||||
@@ -141,25 +131,22 @@ class DialogDeviceRegistryDetail extends LitElement {
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <ha-dialog-footer slot="footer">
 | 
			
		||||
          <ha-button
 | 
			
		||||
            slot="secondaryAction"
 | 
			
		||||
            @click=${this.closeDialog}
 | 
			
		||||
            .disabled=${this._submitting}
 | 
			
		||||
            appearance="plain"
 | 
			
		||||
          >
 | 
			
		||||
            ${this.hass.localize("ui.common.cancel")}
 | 
			
		||||
          </ha-button>
 | 
			
		||||
          <ha-button
 | 
			
		||||
            slot="primaryAction"
 | 
			
		||||
            @click=${this._updateEntry}
 | 
			
		||||
            .disabled=${this._submitting}
 | 
			
		||||
          >
 | 
			
		||||
            ${this.hass.localize("ui.dialogs.device-registry-detail.update")}
 | 
			
		||||
          </ha-button>
 | 
			
		||||
        </ha-dialog-footer>
 | 
			
		||||
      </ha-wa-dialog>
 | 
			
		||||
        <ha-button
 | 
			
		||||
          slot="secondaryAction"
 | 
			
		||||
          @click=${this.closeDialog}
 | 
			
		||||
          .disabled=${this._submitting}
 | 
			
		||||
          appearance="plain"
 | 
			
		||||
        >
 | 
			
		||||
          ${this.hass.localize("ui.common.cancel")}
 | 
			
		||||
        </ha-button>
 | 
			
		||||
        <ha-button
 | 
			
		||||
          slot="primaryAction"
 | 
			
		||||
          @click=${this._updateEntry}
 | 
			
		||||
          .disabled=${this._submitting}
 | 
			
		||||
        >
 | 
			
		||||
          ${this.hass.localize("ui.dialogs.device-registry-detail.update")}
 | 
			
		||||
        </ha-button>
 | 
			
		||||
      </ha-dialog>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -82,7 +82,7 @@ class DialogLabelDetail
 | 
			
		||||
          this.hass,
 | 
			
		||||
          this._params.entry
 | 
			
		||||
            ? this._params.entry.name || this._params.entry.label_id
 | 
			
		||||
            : this.hass!.localize("ui.panel.config.labels.detail.new_label")
 | 
			
		||||
            : this.hass!.localize("ui.dialogs.label-detail.new_label")
 | 
			
		||||
        )}
 | 
			
		||||
      >
 | 
			
		||||
        <div>
 | 
			
		||||
@@ -95,11 +95,9 @@ class DialogLabelDetail
 | 
			
		||||
              .value=${this._name}
 | 
			
		||||
              .configValue=${"name"}
 | 
			
		||||
              @input=${this._input}
 | 
			
		||||
              .label=${this.hass!.localize(
 | 
			
		||||
                "ui.panel.config.labels.detail.name"
 | 
			
		||||
              )}
 | 
			
		||||
              .label=${this.hass!.localize("ui.dialogs.label-detail.name")}
 | 
			
		||||
              .validationMessage=${this.hass!.localize(
 | 
			
		||||
                "ui.panel.config.labels.detail.required_error_msg"
 | 
			
		||||
                "ui.dialogs.label-detail.required_error_msg"
 | 
			
		||||
              )}
 | 
			
		||||
              required
 | 
			
		||||
            ></ha-textfield>
 | 
			
		||||
@@ -108,25 +106,21 @@ class DialogLabelDetail
 | 
			
		||||
              .hass=${this.hass}
 | 
			
		||||
              .configValue=${"icon"}
 | 
			
		||||
              @value-changed=${this._valueChanged}
 | 
			
		||||
              .label=${this.hass!.localize(
 | 
			
		||||
                "ui.panel.config.labels.detail.icon"
 | 
			
		||||
              )}
 | 
			
		||||
              .label=${this.hass!.localize("ui.dialogs.label-detail.icon")}
 | 
			
		||||
            ></ha-icon-picker>
 | 
			
		||||
            <ha-color-picker
 | 
			
		||||
              .value=${this._color}
 | 
			
		||||
              .configValue=${"color"}
 | 
			
		||||
              .hass=${this.hass}
 | 
			
		||||
              @value-changed=${this._valueChanged}
 | 
			
		||||
              .label=${this.hass!.localize(
 | 
			
		||||
                "ui.panel.config.labels.detail.color"
 | 
			
		||||
              )}
 | 
			
		||||
              .label=${this.hass!.localize("ui.dialogs.label-detail.color")}
 | 
			
		||||
            ></ha-color-picker>
 | 
			
		||||
            <ha-textarea
 | 
			
		||||
              .value=${this._description}
 | 
			
		||||
              .configValue=${"description"}
 | 
			
		||||
              @input=${this._input}
 | 
			
		||||
              .label=${this.hass!.localize(
 | 
			
		||||
                "ui.panel.config.labels.detail.description"
 | 
			
		||||
                "ui.dialogs.label-detail.description"
 | 
			
		||||
              )}
 | 
			
		||||
            ></ha-textarea>
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -140,7 +134,7 @@ class DialogLabelDetail
 | 
			
		||||
                @click=${this._deleteEntry}
 | 
			
		||||
                .disabled=${this._submitting}
 | 
			
		||||
              >
 | 
			
		||||
                ${this.hass!.localize("ui.panel.config.labels.detail.delete")}
 | 
			
		||||
                ${this.hass!.localize("ui.common.delete")}
 | 
			
		||||
              </ha-button>
 | 
			
		||||
            `
 | 
			
		||||
          : nothing}
 | 
			
		||||
@@ -150,8 +144,8 @@ class DialogLabelDetail
 | 
			
		||||
          .disabled=${this._submitting || !this._name}
 | 
			
		||||
        >
 | 
			
		||||
          ${this._params.entry
 | 
			
		||||
            ? this.hass!.localize("ui.panel.config.labels.detail.update")
 | 
			
		||||
            : this.hass!.localize("ui.panel.config.labels.detail.create")}
 | 
			
		||||
            ? this.hass!.localize("ui.common.update")
 | 
			
		||||
            : this.hass!.localize("ui.common.create")}
 | 
			
		||||
        </ha-button>
 | 
			
		||||
      </ha-dialog>
 | 
			
		||||
    `;
 | 
			
		||||
 
 | 
			
		||||
@@ -148,7 +148,9 @@ export class AssistPipelineDebug extends LitElement {
 | 
			
		||||
      ).pipeline_runs.reverse();
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      showAlertDialog(this, {
 | 
			
		||||
        title: "Failed to fetch pipeline runs",
 | 
			
		||||
        title: this.hass.localize(
 | 
			
		||||
          "ui.panel.config.voice_assistants.debug.error.fetch_runs"
 | 
			
		||||
        ),
 | 
			
		||||
        text: e.message,
 | 
			
		||||
      });
 | 
			
		||||
      return;
 | 
			
		||||
@@ -176,7 +178,9 @@ export class AssistPipelineDebug extends LitElement {
 | 
			
		||||
      ).events;
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      showAlertDialog(this, {
 | 
			
		||||
        title: "Failed to fetch events",
 | 
			
		||||
        title: this.hass.localize(
 | 
			
		||||
          "ui.panel.config.voice_assistants.debug.error.fetch_events"
 | 
			
		||||
        ),
 | 
			
		||||
        text: e.message,
 | 
			
		||||
      });
 | 
			
		||||
      return;
 | 
			
		||||
 
 | 
			
		||||
@@ -30,16 +30,26 @@ export class AssistPipelineEvents extends LitElement {
 | 
			
		||||
    const run = this._processEvents(this.events);
 | 
			
		||||
    if (!run) {
 | 
			
		||||
      if (this.events.length) {
 | 
			
		||||
        return html`<ha-alert alert-type="error">Error showing run</ha-alert>
 | 
			
		||||
        return html`<ha-alert alert-type="error"
 | 
			
		||||
            >${this.hass.localize(
 | 
			
		||||
              "ui.panel.config.voice_assistants.debug.error.showing_run"
 | 
			
		||||
            )}</ha-alert
 | 
			
		||||
          >
 | 
			
		||||
          <ha-card>
 | 
			
		||||
            <ha-expansion-panel>
 | 
			
		||||
              <span slot="header">Raw</span>
 | 
			
		||||
              <span slot="header"
 | 
			
		||||
                >${this.hass.localize(
 | 
			
		||||
                  "ui.panel.config.voice_assistants.debug.raw"
 | 
			
		||||
                )}</span
 | 
			
		||||
              >
 | 
			
		||||
              <pre>${JSON.stringify(this.events, null, 2)}</pre>
 | 
			
		||||
            </ha-expansion-panel>
 | 
			
		||||
          </ha-card>`;
 | 
			
		||||
      }
 | 
			
		||||
      return html`<ha-alert alert-type="warning"
 | 
			
		||||
        >There were no events in this run.</ha-alert
 | 
			
		||||
        >${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.voice_assistants.debug.no_events"
 | 
			
		||||
        )}</ha-alert
 | 
			
		||||
      >`;
 | 
			
		||||
    }
 | 
			
		||||
    return html`
 | 
			
		||||
 
 | 
			
		||||
@@ -11,31 +11,16 @@ import type { HomeAssistant } from "../../../../types";
 | 
			
		||||
import { formatNumber } from "../../../../common/number/format_number";
 | 
			
		||||
import "../../../../components/ha-yaml-editor";
 | 
			
		||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
 | 
			
		||||
import type { LocalizeKeys } from "../../../../common/translations/localize";
 | 
			
		||||
 | 
			
		||||
const RUN_DATA = {
 | 
			
		||||
  pipeline: "Pipeline",
 | 
			
		||||
  language: "Language",
 | 
			
		||||
};
 | 
			
		||||
const WAKE_WORD_DATA = {
 | 
			
		||||
  engine: "Engine",
 | 
			
		||||
};
 | 
			
		||||
const RUN_DATA = ["pipeline", "language"];
 | 
			
		||||
const WAKE_WORD_DATA = ["engine"];
 | 
			
		||||
 | 
			
		||||
const STT_DATA = {
 | 
			
		||||
  engine: "Engine",
 | 
			
		||||
};
 | 
			
		||||
const STT_DATA = ["engine"];
 | 
			
		||||
 | 
			
		||||
const INTENT_DATA = {
 | 
			
		||||
  engine: "Engine",
 | 
			
		||||
  language: "Language",
 | 
			
		||||
  intent_input: "Input",
 | 
			
		||||
};
 | 
			
		||||
const INTENT_DATA = ["engine", "language", "intent_input"];
 | 
			
		||||
 | 
			
		||||
const TTS_DATA = {
 | 
			
		||||
  engine: "Engine",
 | 
			
		||||
  language: "Language",
 | 
			
		||||
  voice: "Voice",
 | 
			
		||||
  tts_input: "Input",
 | 
			
		||||
};
 | 
			
		||||
const TTS_DATA = ["engine", "language", "voice", "tts_input"];
 | 
			
		||||
 | 
			
		||||
const STAGES: Record<PipelineRun["stage"], number> = {
 | 
			
		||||
  ready: 0,
 | 
			
		||||
@@ -102,24 +87,32 @@ const renderProgress = (
 | 
			
		||||
  return html`${durationString}s ✅`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const renderData = (data: Record<string, any>, keys: Record<string, string>) =>
 | 
			
		||||
  Object.entries(keys).map(
 | 
			
		||||
    ([key, label]) => html`
 | 
			
		||||
const renderData = (
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  data: Record<string, any>,
 | 
			
		||||
  keys: string[]
 | 
			
		||||
) =>
 | 
			
		||||
  keys.map((key) => {
 | 
			
		||||
    const label = hass.localize(
 | 
			
		||||
      `ui.panel.config.voice_assistants.debug.stages.${key}` as LocalizeKeys
 | 
			
		||||
    );
 | 
			
		||||
    return html`
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div>${label}</div>
 | 
			
		||||
        <div>${data[key]}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    `
 | 
			
		||||
  );
 | 
			
		||||
    `;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
const dataMinusKeysRender = (
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  data: Record<string, any>,
 | 
			
		||||
  keys: Record<string, string>
 | 
			
		||||
  keys: string[]
 | 
			
		||||
) => {
 | 
			
		||||
  const result = {};
 | 
			
		||||
  let render = false;
 | 
			
		||||
  for (const key in data) {
 | 
			
		||||
    if (key in keys || key === "done") {
 | 
			
		||||
    if (keys.includes(key) || key === "done") {
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    render = true;
 | 
			
		||||
@@ -127,7 +120,9 @@ const dataMinusKeysRender = (
 | 
			
		||||
  }
 | 
			
		||||
  return render
 | 
			
		||||
    ? html`<ha-expansion-panel>
 | 
			
		||||
        <span slot="header">Raw</span>
 | 
			
		||||
        <span slot="header"
 | 
			
		||||
          >${hass.localize("ui.panel.config.voice_assistants.debug.raw")}</span
 | 
			
		||||
        >
 | 
			
		||||
        <ha-yaml-editor readOnly autoUpdate .value=${result}></ha-yaml-editor>
 | 
			
		||||
      </ha-expansion-panel>`
 | 
			
		||||
    : "";
 | 
			
		||||
@@ -139,6 +134,12 @@ export class AssistPipelineDebug extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: false }) public pipelineRun!: PipelineRun;
 | 
			
		||||
 | 
			
		||||
  private _audioElement?: HTMLAudioElement;
 | 
			
		||||
 | 
			
		||||
  private get _isPlaying(): boolean {
 | 
			
		||||
    return this._audioElement != null && !this._audioElement.paused;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render(): TemplateResult {
 | 
			
		||||
    const lastRunStage: string = this.pipelineRun
 | 
			
		||||
      ? ["tts", "intent", "stt", "wake_word"].find(
 | 
			
		||||
@@ -177,11 +178,15 @@ export class AssistPipelineDebug extends LitElement {
 | 
			
		||||
      <ha-card>
 | 
			
		||||
        <div class="card-content">
 | 
			
		||||
          <div class="row heading">
 | 
			
		||||
            <div>Run</div>
 | 
			
		||||
            <div>
 | 
			
		||||
              ${this.hass.localize(
 | 
			
		||||
                "ui.panel.config.voice_assistants.debug.run"
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
            <div>${this.pipelineRun.stage}</div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          ${renderData(this.pipelineRun.run, RUN_DATA)}
 | 
			
		||||
          ${renderData(this.hass, this.pipelineRun.run, RUN_DATA)}
 | 
			
		||||
          ${messages.length > 0
 | 
			
		||||
            ? html`
 | 
			
		||||
                <div class="messages">
 | 
			
		||||
@@ -203,23 +208,39 @@ export class AssistPipelineDebug extends LitElement {
 | 
			
		||||
            <ha-card>
 | 
			
		||||
              <div class="card-content">
 | 
			
		||||
                <div class="row heading">
 | 
			
		||||
                  <span>Wake word</span>
 | 
			
		||||
                  <span
 | 
			
		||||
                    >${this.hass.localize(
 | 
			
		||||
                      "ui.panel.config.voice_assistants.debug.stages.wake_word"
 | 
			
		||||
                    )}</span
 | 
			
		||||
                  >
 | 
			
		||||
                  ${renderProgress(this.hass, this.pipelineRun, "wake_word")}
 | 
			
		||||
                </div>
 | 
			
		||||
                ${this.pipelineRun.wake_word
 | 
			
		||||
                  ? html`
 | 
			
		||||
                      <div class="card-content">
 | 
			
		||||
                        ${renderData(this.pipelineRun.wake_word, STT_DATA)}
 | 
			
		||||
                        ${renderData(
 | 
			
		||||
                          this.hass,
 | 
			
		||||
                          this.pipelineRun.wake_word,
 | 
			
		||||
                          WAKE_WORD_DATA
 | 
			
		||||
                        )}
 | 
			
		||||
                        ${this.pipelineRun.wake_word.wake_word_output
 | 
			
		||||
                          ? html`<div class="row">
 | 
			
		||||
                                <div>Model</div>
 | 
			
		||||
                                <div>
 | 
			
		||||
                                  ${this.hass.localize(
 | 
			
		||||
                                    "ui.panel.config.voice_assistants.debug.stages.model"
 | 
			
		||||
                                  )}
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div>
 | 
			
		||||
                                  ${this.pipelineRun.wake_word.wake_word_output
 | 
			
		||||
                                    .ww_id}
 | 
			
		||||
                                </div>
 | 
			
		||||
                              </div>
 | 
			
		||||
                              <div class="row">
 | 
			
		||||
                                <div>Timestamp</div>
 | 
			
		||||
                                <div>
 | 
			
		||||
                                  ${this.hass.localize(
 | 
			
		||||
                                    "ui.panel.config.voice_assistants.debug.stages.timestamp"
 | 
			
		||||
                                  )}
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div>
 | 
			
		||||
                                  ${this.pipelineRun.wake_word.wake_word_output
 | 
			
		||||
                                    .timestamp}
 | 
			
		||||
@@ -227,6 +248,7 @@ export class AssistPipelineDebug extends LitElement {
 | 
			
		||||
                              </div>`
 | 
			
		||||
                          : ""}
 | 
			
		||||
                        ${dataMinusKeysRender(
 | 
			
		||||
                          this.hass,
 | 
			
		||||
                          this.pipelineRun.wake_word,
 | 
			
		||||
                          WAKE_WORD_DATA
 | 
			
		||||
                        )}
 | 
			
		||||
@@ -243,7 +265,11 @@ export class AssistPipelineDebug extends LitElement {
 | 
			
		||||
            <ha-card>
 | 
			
		||||
              <div class="card-content">
 | 
			
		||||
                <div class="row heading">
 | 
			
		||||
                  <span>Speech-to-text</span>
 | 
			
		||||
                  <span
 | 
			
		||||
                    >${this.hass.localize(
 | 
			
		||||
                      "ui.panel.config.voice_assistants.debug.stages.speech_to_text"
 | 
			
		||||
                    )}</span
 | 
			
		||||
                  >
 | 
			
		||||
                  ${renderProgress(
 | 
			
		||||
                    this.hass,
 | 
			
		||||
                    this.pipelineRun,
 | 
			
		||||
@@ -254,18 +280,30 @@ export class AssistPipelineDebug extends LitElement {
 | 
			
		||||
                ${this.pipelineRun.stt
 | 
			
		||||
                  ? html`
 | 
			
		||||
                      <div class="card-content">
 | 
			
		||||
                        ${renderData(this.pipelineRun.stt, STT_DATA)}
 | 
			
		||||
                        ${renderData(this.hass, this.pipelineRun.stt, STT_DATA)}
 | 
			
		||||
                        <div class="row">
 | 
			
		||||
                          <div>Language</div>
 | 
			
		||||
                          <div>
 | 
			
		||||
                            ${this.hass.localize(
 | 
			
		||||
                              "ui.panel.config.voice_assistants.debug.stages.language"
 | 
			
		||||
                            )}
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <div>${this.pipelineRun.stt.metadata.language}</div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        ${this.pipelineRun.stt.stt_output
 | 
			
		||||
                          ? html`<div class="row">
 | 
			
		||||
                              <div>Output</div>
 | 
			
		||||
                              <div>
 | 
			
		||||
                                ${this.hass.localize(
 | 
			
		||||
                                  "ui.panel.config.voice_assistants.debug.stages.output"
 | 
			
		||||
                                )}
 | 
			
		||||
                              </div>
 | 
			
		||||
                              <div>${this.pipelineRun.stt.stt_output.text}</div>
 | 
			
		||||
                            </div>`
 | 
			
		||||
                          : ""}
 | 
			
		||||
                        ${dataMinusKeysRender(this.pipelineRun.stt, STT_DATA)}
 | 
			
		||||
                        ${dataMinusKeysRender(
 | 
			
		||||
                          this.hass,
 | 
			
		||||
                          this.pipelineRun.stt,
 | 
			
		||||
                          STT_DATA
 | 
			
		||||
                        )}
 | 
			
		||||
                      </div>
 | 
			
		||||
                    `
 | 
			
		||||
                  : ""}
 | 
			
		||||
@@ -279,16 +317,28 @@ export class AssistPipelineDebug extends LitElement {
 | 
			
		||||
            <ha-card>
 | 
			
		||||
              <div class="card-content">
 | 
			
		||||
                <div class="row heading">
 | 
			
		||||
                  <span>Natural Language Processing</span>
 | 
			
		||||
                  <span
 | 
			
		||||
                    >${this.hass.localize(
 | 
			
		||||
                      "ui.panel.config.voice_assistants.debug.stages.natural_language_processing"
 | 
			
		||||
                    )}</span
 | 
			
		||||
                  >
 | 
			
		||||
                  ${renderProgress(this.hass, this.pipelineRun, "intent")}
 | 
			
		||||
                </div>
 | 
			
		||||
                ${this.pipelineRun.intent
 | 
			
		||||
                  ? html`
 | 
			
		||||
                      <div class="card-content">
 | 
			
		||||
                        ${renderData(this.pipelineRun.intent, INTENT_DATA)}
 | 
			
		||||
                        ${renderData(
 | 
			
		||||
                          this.hass,
 | 
			
		||||
                          this.pipelineRun.intent,
 | 
			
		||||
                          INTENT_DATA
 | 
			
		||||
                        )}
 | 
			
		||||
                        ${this.pipelineRun.intent.intent_output
 | 
			
		||||
                          ? html`<div class="row">
 | 
			
		||||
                                <div>Response type</div>
 | 
			
		||||
                                <div>
 | 
			
		||||
                                  ${this.hass.localize(
 | 
			
		||||
                                    "ui.panel.config.voice_assistants.debug.stages.response_type"
 | 
			
		||||
                                  )}
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div>
 | 
			
		||||
                                  ${this.pipelineRun.intent.intent_output
 | 
			
		||||
                                    .response.response_type}
 | 
			
		||||
@@ -297,7 +347,11 @@ export class AssistPipelineDebug extends LitElement {
 | 
			
		||||
                              ${this.pipelineRun.intent.intent_output.response
 | 
			
		||||
                                .response_type === "error"
 | 
			
		||||
                                ? html`<div class="row">
 | 
			
		||||
                                    <div>Error code</div>
 | 
			
		||||
                                    <div>
 | 
			
		||||
                                      ${this.hass.localize(
 | 
			
		||||
                                        "ui.panel.config.voice_assistants.debug.error.code"
 | 
			
		||||
                                      )}
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div>
 | 
			
		||||
                                      ${this.pipelineRun.intent.intent_output
 | 
			
		||||
                                        .response.data.code}
 | 
			
		||||
@@ -306,18 +360,27 @@ export class AssistPipelineDebug extends LitElement {
 | 
			
		||||
                                : ""}`
 | 
			
		||||
                          : ""}
 | 
			
		||||
                        <div class="row">
 | 
			
		||||
                          <div>Prefer handling locally</div>
 | 
			
		||||
                          <div>
 | 
			
		||||
                            ${this.hass.localize(
 | 
			
		||||
                              "ui.panel.config.voice_assistants.debug.stages.prefer_local"
 | 
			
		||||
                            )}
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <div>
 | 
			
		||||
                            ${this.pipelineRun.intent.prefer_local_intents}
 | 
			
		||||
                          </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="row">
 | 
			
		||||
                          <div>Processed locally</div>
 | 
			
		||||
                          <div>
 | 
			
		||||
                            ${this.hass.localize(
 | 
			
		||||
                              "ui.panel.config.voice_assistants.debug.stages.processed_locally"
 | 
			
		||||
                            )}
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <div>
 | 
			
		||||
                            ${this.pipelineRun.intent.processed_locally}
 | 
			
		||||
                          </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        ${dataMinusKeysRender(
 | 
			
		||||
                          this.hass,
 | 
			
		||||
                          this.pipelineRun.intent,
 | 
			
		||||
                          INTENT_DATA
 | 
			
		||||
                        )}
 | 
			
		||||
@@ -334,14 +397,22 @@ export class AssistPipelineDebug extends LitElement {
 | 
			
		||||
            <ha-card>
 | 
			
		||||
              <div class="card-content">
 | 
			
		||||
                <div class="row heading">
 | 
			
		||||
                  <span>Text-to-speech</span>
 | 
			
		||||
                  <span
 | 
			
		||||
                    >${this.hass.localize(
 | 
			
		||||
                      "ui.panel.config.voice_assistants.debug.stages.text_to_speech"
 | 
			
		||||
                    )}</span
 | 
			
		||||
                  >
 | 
			
		||||
                  ${renderProgress(this.hass, this.pipelineRun, "tts")}
 | 
			
		||||
                </div>
 | 
			
		||||
                ${this.pipelineRun.tts
 | 
			
		||||
                  ? html`
 | 
			
		||||
                      <div class="card-content">
 | 
			
		||||
                        ${renderData(this.pipelineRun.tts, TTS_DATA)}
 | 
			
		||||
                        ${dataMinusKeysRender(this.pipelineRun.tts, TTS_DATA)}
 | 
			
		||||
                        ${renderData(this.hass, this.pipelineRun.tts, TTS_DATA)}
 | 
			
		||||
                        ${dataMinusKeysRender(
 | 
			
		||||
                          this.hass,
 | 
			
		||||
                          this.pipelineRun.tts,
 | 
			
		||||
                          TTS_DATA
 | 
			
		||||
                        )}
 | 
			
		||||
                      </div>
 | 
			
		||||
                    `
 | 
			
		||||
                  : ""}
 | 
			
		||||
@@ -349,8 +420,19 @@ export class AssistPipelineDebug extends LitElement {
 | 
			
		||||
              ${this.pipelineRun?.tts?.tts_output
 | 
			
		||||
                ? html`
 | 
			
		||||
                    <div class="card-actions">
 | 
			
		||||
                      <ha-button @click=${this._playTTS}>
 | 
			
		||||
                        Play Audio
 | 
			
		||||
                      <ha-button
 | 
			
		||||
                        .variant=${this._isPlaying ? "danger" : "brand"}
 | 
			
		||||
                        @click=${this._isPlaying
 | 
			
		||||
                          ? this._stopTTS
 | 
			
		||||
                          : this._playTTS}
 | 
			
		||||
                      >
 | 
			
		||||
                        ${this._isPlaying
 | 
			
		||||
                          ? this.hass.localize(
 | 
			
		||||
                              "ui.panel.config.voice_assistants.debug.stop_audio"
 | 
			
		||||
                            )
 | 
			
		||||
                          : this.hass.localize(
 | 
			
		||||
                              "ui.panel.config.voice_assistants.debug.play_audio"
 | 
			
		||||
                            )}
 | 
			
		||||
                      </ha-button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  `
 | 
			
		||||
@@ -361,7 +443,11 @@ export class AssistPipelineDebug extends LitElement {
 | 
			
		||||
      ${maybeRenderError(this.pipelineRun, "tts", lastRunStage)}
 | 
			
		||||
      <ha-card>
 | 
			
		||||
        <ha-expansion-panel>
 | 
			
		||||
          <span slot="header">Raw</span>
 | 
			
		||||
          <span slot="header"
 | 
			
		||||
            >${this.hass.localize(
 | 
			
		||||
              "ui.panel.config.voice_assistants.debug.raw"
 | 
			
		||||
            )}</span
 | 
			
		||||
          >
 | 
			
		||||
          <ha-yaml-editor
 | 
			
		||||
            read-only
 | 
			
		||||
            auto-update
 | 
			
		||||
@@ -373,14 +459,48 @@ export class AssistPipelineDebug extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _playTTS(): void {
 | 
			
		||||
    // Stop any existing audio first
 | 
			
		||||
    this._stopTTS();
 | 
			
		||||
 | 
			
		||||
    const url = this.pipelineRun!.tts!.tts_output!.url;
 | 
			
		||||
    const audio = new Audio(url);
 | 
			
		||||
    audio.addEventListener("error", () => {
 | 
			
		||||
      showAlertDialog(this, { title: "Error", text: "Error playing audio" });
 | 
			
		||||
    this._audioElement = new Audio(url);
 | 
			
		||||
 | 
			
		||||
    this._audioElement.addEventListener("error", () => {
 | 
			
		||||
      showAlertDialog(this, {
 | 
			
		||||
        title: this.hass.localize(
 | 
			
		||||
          "ui.panel.config.voice_assistants.debug.error.title"
 | 
			
		||||
        ),
 | 
			
		||||
        text: this.hass.localize(
 | 
			
		||||
          "ui.panel.config.voice_assistants.debug.error.playing_audio"
 | 
			
		||||
        ),
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    audio.addEventListener("canplaythrough", () => {
 | 
			
		||||
      audio.play();
 | 
			
		||||
 | 
			
		||||
    this._audioElement.addEventListener("play", () => {
 | 
			
		||||
      this.requestUpdate();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this._audioElement.addEventListener("ended", () => {
 | 
			
		||||
      this.requestUpdate();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this._audioElement.addEventListener("canplaythrough", () => {
 | 
			
		||||
      this._audioElement!.play();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _stopTTS(): void {
 | 
			
		||||
    if (this._audioElement) {
 | 
			
		||||
      this._audioElement.pause();
 | 
			
		||||
      this._audioElement.currentTime = 0;
 | 
			
		||||
      this._audioElement = undefined;
 | 
			
		||||
      this.requestUpdate();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public disconnectedCallback(): void {
 | 
			
		||||
    super.disconnectedCallback();
 | 
			
		||||
    this._stopTTS();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static styles = css`
 | 
			
		||||
 
 | 
			
		||||
@@ -52,6 +52,8 @@ export class HaLogbook extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: false }) public deviceIds?: string[];
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: false }) public stateFilter?: string[];
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public narrow = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, reflect: true }) public virtualize = false;
 | 
			
		||||
@@ -165,7 +167,7 @@ export class HaLogbook extends LitElement {
 | 
			
		||||
  protected willUpdate(changedProps: PropertyValues): void {
 | 
			
		||||
    let changed = changedProps.has("time");
 | 
			
		||||
 | 
			
		||||
    for (const key of ["entityIds", "deviceIds"]) {
 | 
			
		||||
    for (const key of ["entityIds", "deviceIds", "stateFilter"]) {
 | 
			
		||||
      if (!changedProps.has(key)) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
@@ -352,9 +354,19 @@ export class HaLogbook extends LitElement {
 | 
			
		||||
      "recent" in this.time
 | 
			
		||||
        ? findStartOfRecentTime(new Date(), this.time.recent)
 | 
			
		||||
        : undefined;
 | 
			
		||||
 | 
			
		||||
    let eventsFiltered: LogbookEntry[] | undefined;
 | 
			
		||||
    if (this.stateFilter && this.stateFilter.length > 0) {
 | 
			
		||||
      eventsFiltered = streamMessage.events.filter(
 | 
			
		||||
        (e) => e.state && this.stateFilter?.includes(e.state)
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      eventsFiltered = [...streamMessage.events];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Put newest ones on top. Reverse works in-place so
 | 
			
		||||
    // make a copy first.
 | 
			
		||||
    const newEntries = [...streamMessage.events].reverse();
 | 
			
		||||
    const newEntries = eventsFiltered.reverse();
 | 
			
		||||
    if (!this._logbookEntries || !this._logbookEntries.length) {
 | 
			
		||||
      this._logbookEntries = newEntries;
 | 
			
		||||
      return;
 | 
			
		||||
 
 | 
			
		||||
@@ -94,7 +94,7 @@ class HuiHistoryChartCardFeature
 | 
			
		||||
    }
 | 
			
		||||
    if (!this._coordinates) {
 | 
			
		||||
      return html`
 | 
			
		||||
        <div class="container">
 | 
			
		||||
        <div class="container loading">
 | 
			
		||||
          <ha-spinner size="small"></ha-spinner>
 | 
			
		||||
        </div>
 | 
			
		||||
      `;
 | 
			
		||||
@@ -153,6 +153,14 @@ class HuiHistoryChartCardFeature
 | 
			
		||||
      align-items: flex-end;
 | 
			
		||||
      pointer-events: none !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .container.loading {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hui-graph-base {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      --accent-color: var(--feature-color);
 | 
			
		||||
 
 | 
			
		||||
@@ -80,41 +80,44 @@ export class HuiEnergyCompareCard
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <ha-alert dismissable @alert-dismissed-clicked=${this._stopCompare}>
 | 
			
		||||
        ${this.hass.localize("ui.panel.energy.compare.info", {
 | 
			
		||||
          start: html`<b
 | 
			
		||||
            >${formatDate(
 | 
			
		||||
              this._start!,
 | 
			
		||||
              this.hass.locale,
 | 
			
		||||
              this.hass.config
 | 
			
		||||
            )}${dayDifference > 0
 | 
			
		||||
              ? ` -
 | 
			
		||||
        ${this.hass.localize(
 | 
			
		||||
          "ui.panel.lovelace.cards.energy.energy_compare.info",
 | 
			
		||||
          {
 | 
			
		||||
            start: html`<b
 | 
			
		||||
              >${formatDate(
 | 
			
		||||
                this._start!,
 | 
			
		||||
                this.hass.locale,
 | 
			
		||||
                this.hass.config
 | 
			
		||||
              )}${dayDifference > 0
 | 
			
		||||
                ? ` -
 | 
			
		||||
          ${formatDate(
 | 
			
		||||
            this._end || endOfDay(new Date()),
 | 
			
		||||
            this.hass.locale,
 | 
			
		||||
            this.hass.config
 | 
			
		||||
          )}`
 | 
			
		||||
              : ""}</b
 | 
			
		||||
          >`,
 | 
			
		||||
          end: html`<b
 | 
			
		||||
              >${formatDate(
 | 
			
		||||
                this._startCompare,
 | 
			
		||||
                this.hass.locale,
 | 
			
		||||
                this.hass.config
 | 
			
		||||
              )}${dayDifference > 0
 | 
			
		||||
                ? ` -
 | 
			
		||||
          ${formatDate(this._endCompare, this.hass.locale, this.hass.config)}`
 | 
			
		||||
                : ""}</b
 | 
			
		||||
            >
 | 
			
		||||
            <button class="link" @click=${this._changeCompareMode}>
 | 
			
		||||
              (${this._compareMode === CompareMode.PREVIOUS
 | 
			
		||||
                ? this.hass.localize(
 | 
			
		||||
                    "ui.panel.energy.compare.compare_previous_year"
 | 
			
		||||
                  )
 | 
			
		||||
                : this.hass.localize(
 | 
			
		||||
                    "ui.panel.energy.compare.compare_previous_period"
 | 
			
		||||
                  )})
 | 
			
		||||
            </button>`,
 | 
			
		||||
        })}
 | 
			
		||||
            >`,
 | 
			
		||||
            end: html`<b
 | 
			
		||||
                >${formatDate(
 | 
			
		||||
                  this._startCompare,
 | 
			
		||||
                  this.hass.locale,
 | 
			
		||||
                  this.hass.config
 | 
			
		||||
                )}${dayDifference > 0
 | 
			
		||||
                  ? ` -
 | 
			
		||||
          ${formatDate(this._endCompare, this.hass.locale, this.hass.config)}`
 | 
			
		||||
                  : ""}</b
 | 
			
		||||
              >
 | 
			
		||||
              <button class="link" @click=${this._changeCompareMode}>
 | 
			
		||||
                (${this._compareMode === CompareMode.PREVIOUS
 | 
			
		||||
                  ? this.hass.localize(
 | 
			
		||||
                      "ui.panel.lovelace.cards.energy.energy_compare.compare_previous_year"
 | 
			
		||||
                    )
 | 
			
		||||
                  : this.hass.localize(
 | 
			
		||||
                      "ui.panel.lovelace.cards.energy.energy_compare.compare_previous_period"
 | 
			
		||||
                    )})
 | 
			
		||||
              </button>`,
 | 
			
		||||
          }
 | 
			
		||||
        )}
 | 
			
		||||
      </ha-alert>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -137,6 +137,7 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
 | 
			
		||||
          class=${classMap({
 | 
			
		||||
            "is-grid": this.layout === "grid",
 | 
			
		||||
            "is-panel": this.layout === "panel",
 | 
			
		||||
            "has-title": !!this._config.title,
 | 
			
		||||
          })}
 | 
			
		||||
          .narrow=${this._narrow}
 | 
			
		||||
          .events=${this._events}
 | 
			
		||||
@@ -229,6 +230,7 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
 | 
			
		||||
      padding: 0 8px 8px;
 | 
			
		||||
      box-sizing: border-box;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .header {
 | 
			
		||||
@@ -239,15 +241,25 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
 | 
			
		||||
      padding-left: 8px;
 | 
			
		||||
      padding-inline-start: 8px;
 | 
			
		||||
      direction: var(--direction);
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
      text-overflow: ellipsis;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ha-full-calendar {
 | 
			
		||||
      --calendar-height: 400px;
 | 
			
		||||
      height: var(--calendar-height);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ha-full-calendar.is-grid,
 | 
			
		||||
    ha-full-calendar.is-panel {
 | 
			
		||||
      height: calc(100% - 16px);
 | 
			
		||||
      --calendar-height: calc(100% - 16px);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ha-full-calendar.is-grid.has-title,
 | 
			
		||||
    ha-full-calendar.is-panel.has-title {
 | 
			
		||||
      --calendar-height: calc(
 | 
			
		||||
        100% - var(--ha-card-header-font-size, var(--ha-font-size-2xl)) - 22px
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  `;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import type { PropertyValues, TemplateResult } from "lit";
 | 
			
		||||
import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
import { customElement, 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";
 | 
			
		||||
@@ -20,11 +20,9 @@ import type {
 | 
			
		||||
import type {
 | 
			
		||||
  LovelaceCard,
 | 
			
		||||
  LovelaceCardEditor,
 | 
			
		||||
  LovelaceGridOptions,
 | 
			
		||||
  LovelaceHeaderFooter,
 | 
			
		||||
} from "../types";
 | 
			
		||||
import type { EntitiesCardConfig } from "./types";
 | 
			
		||||
import { haStyleScrollbar } from "../../../resources/styles";
 | 
			
		||||
 | 
			
		||||
export const computeShowHeaderToggle = <
 | 
			
		||||
  T extends EntityConfig | LovelaceRowConfig,
 | 
			
		||||
@@ -77,8 +75,6 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
 | 
			
		||||
 | 
			
		||||
  private _hass?: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: false }) public layout?: string;
 | 
			
		||||
 | 
			
		||||
  private _configEntities?: LovelaceRowConfig[];
 | 
			
		||||
 | 
			
		||||
  private _showHeaderToggle?: boolean;
 | 
			
		||||
@@ -143,14 +139,6 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
 | 
			
		||||
    return size;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getGridOptions(): LovelaceGridOptions {
 | 
			
		||||
    return {
 | 
			
		||||
      columns: 12,
 | 
			
		||||
      min_columns: 6,
 | 
			
		||||
      min_rows: this._config?.title || this._showHeaderToggle ? 3 : 2,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setConfig(config: EntitiesCardConfig): void {
 | 
			
		||||
    if (!config.entities || !Array.isArray(config.entities)) {
 | 
			
		||||
      throw new Error("Entities must be specified");
 | 
			
		||||
@@ -245,7 +233,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
 | 
			
		||||
                    `}
 | 
			
		||||
              </h1>
 | 
			
		||||
            `}
 | 
			
		||||
        <div id="states" class="card-content ha-scrollbar">
 | 
			
		||||
        <div id="states" class="card-content">
 | 
			
		||||
          ${this._configEntities!.map((entityConf) =>
 | 
			
		||||
            this._renderEntity(entityConf)
 | 
			
		||||
          )}
 | 
			
		||||
@@ -258,73 +246,69 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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;
 | 
			
		||||
      }
 | 
			
		||||
  static styles = 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;
 | 
			
		||||
      }
 | 
			
		||||
    .card-header .name {
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      text-overflow: ellipsis;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      #states {
 | 
			
		||||
        flex: 1;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        gap: var(--entities-card-row-gap, var(--card-row-gap, 8px));
 | 
			
		||||
        overflow-y: auto;
 | 
			
		||||
      }
 | 
			
		||||
    #states {
 | 
			
		||||
      flex: 1;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      gap: var(--entities-card-row-gap, var(--card-row-gap, 8px));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      #states > div > * {
 | 
			
		||||
        overflow: clip visible;
 | 
			
		||||
      }
 | 
			
		||||
    #states > div > * {
 | 
			
		||||
      overflow: clip visible;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      #states > div {
 | 
			
		||||
        position: relative;
 | 
			
		||||
      }
 | 
			
		||||
    #states > div {
 | 
			
		||||
      position: relative;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      .icon {
 | 
			
		||||
        padding: 0px 18px 0px 8px;
 | 
			
		||||
      }
 | 
			
		||||
    .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)
 | 
			
		||||
        );
 | 
			
		||||
        margin-bottom: 16px;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
      }
 | 
			
		||||
    .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)
 | 
			
		||||
      );
 | 
			
		||||
      margin-bottom: 16px;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      .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;
 | 
			
		||||
      }
 | 
			
		||||
    `,
 | 
			
		||||
  ];
 | 
			
		||||
    .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 _renderEntity(entityConf: LovelaceRowConfig): TemplateResult {
 | 
			
		||||
    const element = createRowElement(
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@ import { classMap } from "lit/directives/class-map";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined";
 | 
			
		||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
 | 
			
		||||
import { computeDomain } from "../../../common/entity/compute_domain";
 | 
			
		||||
import { computeStateName } from "../../../common/entity/compute_state_name";
 | 
			
		||||
import "../../../components/entity/state-badge";
 | 
			
		||||
import "../../../components/ha-card";
 | 
			
		||||
import "../../../components/ha-icon";
 | 
			
		||||
@@ -19,6 +18,7 @@ import type {
 | 
			
		||||
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import { actionHandler } from "../common/directives/action-handler-directive";
 | 
			
		||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
 | 
			
		||||
import { findEntities } from "../common/find-entities";
 | 
			
		||||
import { handleAction } from "../common/handle-action";
 | 
			
		||||
import { hasAction, hasAnyAction } from "../common/has-action";
 | 
			
		||||
@@ -252,7 +252,11 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
 | 
			
		||||
      </div>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const name = entityConf.name ?? computeStateName(stateObj);
 | 
			
		||||
    const name = computeLovelaceEntityName(
 | 
			
		||||
      this.hass!,
 | 
			
		||||
      stateObj,
 | 
			
		||||
      entityConf.name
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <div
 | 
			
		||||
 
 | 
			
		||||
@@ -3,25 +3,27 @@ import { LitElement, css, html, nothing } from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
import { classMap } from "lit/directives/class-map";
 | 
			
		||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
 | 
			
		||||
import { createSearchParam } from "../../../common/url/search-params";
 | 
			
		||||
import "../../../components/chart/state-history-charts";
 | 
			
		||||
import "../../../components/ha-alert";
 | 
			
		||||
import "../../../components/ha-card";
 | 
			
		||||
import "../../../components/ha-icon-next";
 | 
			
		||||
import {
 | 
			
		||||
  computeHistory,
 | 
			
		||||
  subscribeHistoryStatesTimeWindow,
 | 
			
		||||
  type HistoryResult,
 | 
			
		||||
  convertStatisticsToHistory,
 | 
			
		||||
  mergeHistoryResults,
 | 
			
		||||
  subscribeHistoryStatesTimeWindow,
 | 
			
		||||
  type HistoryResult,
 | 
			
		||||
} from "../../../data/history";
 | 
			
		||||
import { fetchStatistics } from "../../../data/recorder";
 | 
			
		||||
import { getSensorNumericDeviceClasses } from "../../../data/sensor";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
 | 
			
		||||
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
 | 
			
		||||
import { processConfigEntities } from "../common/process-config-entities";
 | 
			
		||||
import type { EntityConfig } from "../entity-rows/types";
 | 
			
		||||
import type { LovelaceCard, LovelaceGridOptions } from "../types";
 | 
			
		||||
import type { HistoryGraphCardConfig } from "./types";
 | 
			
		||||
import { createSearchParam } from "../../../common/url/search-params";
 | 
			
		||||
import { fetchStatistics } from "../../../data/recorder";
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_HOURS_TO_SHOW = 24;
 | 
			
		||||
 | 
			
		||||
@@ -51,6 +53,8 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
 | 
			
		||||
 | 
			
		||||
  private _entityIds: string[] = [];
 | 
			
		||||
 | 
			
		||||
  private _entities: EntityConfig[] = [];
 | 
			
		||||
 | 
			
		||||
  private _hoursToShow = DEFAULT_HOURS_TO_SHOW;
 | 
			
		||||
 | 
			
		||||
  private _interval?: number;
 | 
			
		||||
@@ -80,21 +84,35 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
 | 
			
		||||
      throw new Error("You must include at least one entity");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const configEntities = config.entities
 | 
			
		||||
    this._entities = config.entities
 | 
			
		||||
      ? processConfigEntities(config.entities)
 | 
			
		||||
      : [];
 | 
			
		||||
 | 
			
		||||
    this._entityIds = [];
 | 
			
		||||
    configEntities.forEach((entity) => {
 | 
			
		||||
      this._entityIds.push(entity.entity);
 | 
			
		||||
      if (entity.name) {
 | 
			
		||||
        this._names[entity.entity] = entity.name;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    this._entityIds = this._entities.map((entity) => entity.entity);
 | 
			
		||||
 | 
			
		||||
    this._hoursToShow = config.hours_to_show || DEFAULT_HOURS_TO_SHOW;
 | 
			
		||||
 | 
			
		||||
    this._config = config;
 | 
			
		||||
    this._computeNames();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _computeNames() {
 | 
			
		||||
    if (!this.hass || !this._config) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this._names = {};
 | 
			
		||||
    this._entities.forEach((entity) => {
 | 
			
		||||
      const stateObj = this.hass!.states[entity.entity];
 | 
			
		||||
      this._names[entity.entity] = stateObj
 | 
			
		||||
        ? computeLovelaceEntityName(this.hass!, stateObj, entity.name)
 | 
			
		||||
        : entity.entity;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public willUpdate(changedProps: PropertyValues) {
 | 
			
		||||
    super.willUpdate(changedProps);
 | 
			
		||||
    if (changedProps.has("hass")) {
 | 
			
		||||
      this._computeNames();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public connectedCallback() {
 | 
			
		||||
 
 | 
			
		||||
@@ -64,6 +64,8 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
 | 
			
		||||
 | 
			
		||||
  @state() private _targetPickerValue: HassServiceTarget = {};
 | 
			
		||||
 | 
			
		||||
  @state() private _stateFilter?: string[];
 | 
			
		||||
 | 
			
		||||
  public getCardSize(): number {
 | 
			
		||||
    return 9 + (this._config?.title ? 1 : 0);
 | 
			
		||||
  }
 | 
			
		||||
@@ -129,6 +131,8 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this._targetPickerValue = target;
 | 
			
		||||
 | 
			
		||||
    this._stateFilter = ensureArray(config.state_filter);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _getEntityIds(): string[] | undefined {
 | 
			
		||||
@@ -209,6 +213,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
 | 
			
		||||
            .hass=${this.hass}
 | 
			
		||||
            .time=${this._time}
 | 
			
		||||
            .entityIds=${this._getEntityIds()}
 | 
			
		||||
            .stateFilter=${this._stateFilter}
 | 
			
		||||
            narrow
 | 
			
		||||
            relative-time
 | 
			
		||||
            virtualize
 | 
			
		||||
 
 | 
			
		||||
@@ -35,20 +35,12 @@ import {
 | 
			
		||||
  hasConfigOrEntitiesChanged,
 | 
			
		||||
} from "../common/has-changed";
 | 
			
		||||
import { processConfigEntities } from "../common/process-config-entities";
 | 
			
		||||
import type { EntityConfig } from "../entity-rows/types";
 | 
			
		||||
import type { LovelaceCard, LovelaceGridOptions } from "../types";
 | 
			
		||||
import type { MapCardConfig } from "./types";
 | 
			
		||||
import type { MapCardConfig, MapEntityConfig } from "./types";
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_HOURS_TO_SHOW = 0;
 | 
			
		||||
export const DEFAULT_ZOOM = 14;
 | 
			
		||||
 | 
			
		||||
interface MapEntityConfig extends EntityConfig {
 | 
			
		||||
  label_mode?: "state" | "attribute" | "name";
 | 
			
		||||
  attribute?: string;
 | 
			
		||||
  unit?: string;
 | 
			
		||||
  focus?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface GeoEntity {
 | 
			
		||||
  entity_id: string;
 | 
			
		||||
  label_mode?: "state" | "attribute" | "name" | "icon";
 | 
			
		||||
 
 | 
			
		||||
@@ -20,14 +20,15 @@ import {
 | 
			
		||||
} from "../../../data/recorder";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import { computeCardSize } from "../common/compute-card-size";
 | 
			
		||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
 | 
			
		||||
import { findEntities } from "../common/find-entities";
 | 
			
		||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
 | 
			
		||||
import { createHeaderFooterElement } from "../create-element/create-header-footer-element";
 | 
			
		||||
import type {
 | 
			
		||||
  LovelaceCard,
 | 
			
		||||
  LovelaceCardEditor,
 | 
			
		||||
  LovelaceHeaderFooter,
 | 
			
		||||
  LovelaceGridOptions,
 | 
			
		||||
  LovelaceHeaderFooter,
 | 
			
		||||
} from "../types";
 | 
			
		||||
import type { HuiErrorCard } from "./hui-error-card";
 | 
			
		||||
import type { EntityCardConfig, StatisticCardConfig } from "./types";
 | 
			
		||||
@@ -180,7 +181,9 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard {
 | 
			
		||||
 | 
			
		||||
    const stateObj = this.hass.states[this._config.entity];
 | 
			
		||||
    const name =
 | 
			
		||||
      this._config.name ||
 | 
			
		||||
      (this._config.name
 | 
			
		||||
        ? computeLovelaceEntityName(this.hass, stateObj, this._config.name)
 | 
			
		||||
        : "") ||
 | 
			
		||||
      getStatisticLabel(this.hass, this._config.entity, this._metadata);
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,11 @@
 | 
			
		||||
import { differenceInDays, subHours } from "date-fns";
 | 
			
		||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
			
		||||
import { subHours, differenceInDays } from "date-fns";
 | 
			
		||||
import type { PropertyValues } from "lit";
 | 
			
		||||
import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
import { classMap } from "lit/directives/class-map";
 | 
			
		||||
import "../../../components/ha-card";
 | 
			
		||||
import { getEnergyDataCollection } from "../../../data/energy";
 | 
			
		||||
import {
 | 
			
		||||
  getSuggestedMax,
 | 
			
		||||
  getSuggestedPeriod,
 | 
			
		||||
} from "./energy/common/energy-chart-options";
 | 
			
		||||
import type {
 | 
			
		||||
  Statistics,
 | 
			
		||||
  StatisticsMetaData,
 | 
			
		||||
@@ -21,10 +17,16 @@ import {
 | 
			
		||||
  getStatisticMetadata,
 | 
			
		||||
} from "../../../data/recorder";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
 | 
			
		||||
import { findEntities } from "../common/find-entities";
 | 
			
		||||
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
 | 
			
		||||
import { processConfigEntities } from "../common/process-config-entities";
 | 
			
		||||
import type { EntityConfig } from "../entity-rows/types";
 | 
			
		||||
import type { LovelaceCard, LovelaceGridOptions } from "../types";
 | 
			
		||||
import {
 | 
			
		||||
  getSuggestedMax,
 | 
			
		||||
  getSuggestedPeriod,
 | 
			
		||||
} from "./energy/common/energy-chart-options";
 | 
			
		||||
import type { StatisticsGraphCardConfig } from "./types";
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_DAYS_TO_SHOW = 30;
 | 
			
		||||
@@ -67,7 +69,9 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
 | 
			
		||||
 | 
			
		||||
  @state() private _unit?: string;
 | 
			
		||||
 | 
			
		||||
  private _entities: string[] = [];
 | 
			
		||||
  private _entities: EntityConfig[] = [];
 | 
			
		||||
 | 
			
		||||
  private _entityIds: string[] = [];
 | 
			
		||||
 | 
			
		||||
  private _names: Record<string, string> = {};
 | 
			
		||||
 | 
			
		||||
@@ -148,17 +152,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
 | 
			
		||||
      throw new Error("You must include at least one entity");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const configEntities = config.entities
 | 
			
		||||
    this._entities = config.entities
 | 
			
		||||
      ? processConfigEntities(config.entities, false)
 | 
			
		||||
      : [];
 | 
			
		||||
 | 
			
		||||
    this._entities = [];
 | 
			
		||||
    configEntities.forEach((entity) => {
 | 
			
		||||
      this._entities.push(entity.entity);
 | 
			
		||||
      if (entity.name) {
 | 
			
		||||
        this._names[entity.entity] = entity.name;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    this._entityIds = this._entities.map((ent) => ent.entity);
 | 
			
		||||
 | 
			
		||||
    if (typeof config.stat_types === "string") {
 | 
			
		||||
      this._statTypes = [config.stat_types];
 | 
			
		||||
@@ -168,6 +165,20 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
 | 
			
		||||
      this._statTypes = config.stat_types;
 | 
			
		||||
    }
 | 
			
		||||
    this._config = config;
 | 
			
		||||
    this._computeNames();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _computeNames() {
 | 
			
		||||
    if (!this.hass || !this._config) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this._names = {};
 | 
			
		||||
    this._entities.forEach((config) => {
 | 
			
		||||
      const stateObj = this.hass!.states[config.entity];
 | 
			
		||||
      this._names[config.entity] = stateObj
 | 
			
		||||
        ? computeLovelaceEntityName(this.hass!, stateObj, config.name)
 | 
			
		||||
        : config.entity;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected shouldUpdate(changedProps: PropertyValues): boolean {
 | 
			
		||||
@@ -209,6 +220,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (changedProps.has("hass")) {
 | 
			
		||||
      this._computeNames();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      changedProps.has("_config") &&
 | 
			
		||||
      oldConfig?.entities !== this._config.entities
 | 
			
		||||
@@ -232,7 +247,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
 | 
			
		||||
    clearInterval(this._interval);
 | 
			
		||||
    this._interval = 0; // block concurrent calls
 | 
			
		||||
    if (fetchMetadata) {
 | 
			
		||||
      await this._getStatisticsMetaData(this._entities);
 | 
			
		||||
      await this._getStatisticsMetaData(this._entityIds);
 | 
			
		||||
    }
 | 
			
		||||
    await this._getStatistics();
 | 
			
		||||
    // statistics are created every hour
 | 
			
		||||
@@ -344,7 +359,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (!unitClass && this._metadata) {
 | 
			
		||||
        const metadata = this._metadata[this._entities[0]];
 | 
			
		||||
        const metadata = this._metadata[this._entityIds[0]];
 | 
			
		||||
        unitClass = metadata?.unit_class;
 | 
			
		||||
        this._unit = unitClass
 | 
			
		||||
          ? getDisplayUnit(this.hass!, metadata.statistic_id, metadata) ||
 | 
			
		||||
@@ -356,14 +371,15 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
 | 
			
		||||
        this.hass!,
 | 
			
		||||
        startDate,
 | 
			
		||||
        endDate,
 | 
			
		||||
        this._entities,
 | 
			
		||||
        this._entityIds,
 | 
			
		||||
        this._period,
 | 
			
		||||
        unitconfig,
 | 
			
		||||
        this._statTypes
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this._statistics = {};
 | 
			
		||||
      this._entities.forEach((id) => {
 | 
			
		||||
      this._entities.forEach((entity) => {
 | 
			
		||||
        const id = entity.entity;
 | 
			
		||||
        if (id in statistics) {
 | 
			
		||||
          this._statistics![id] = statistics[id];
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import type { EnergySourceByType } from "../../../data/energy";
 | 
			
		||||
import type { ActionConfig } from "../../../data/lovelace/config/action";
 | 
			
		||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
 | 
			
		||||
import type { Statistic, StatisticType } from "../../../data/recorder";
 | 
			
		||||
import type { MediaSelectorValue } from "../../../data/selector";
 | 
			
		||||
import type { TimeFormat } from "../../../data/translation";
 | 
			
		||||
import type { ForecastType } from "../../../data/weather";
 | 
			
		||||
import type {
 | 
			
		||||
@@ -29,7 +30,6 @@ import type {
 | 
			
		||||
import type { LovelaceHeaderFooterConfig } from "../header-footer/types";
 | 
			
		||||
import type { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
 | 
			
		||||
import type { HomeSummary } from "../strategies/home/helpers/home-summaries";
 | 
			
		||||
import type { MediaSelectorValue } from "../../../data/selector";
 | 
			
		||||
 | 
			
		||||
export type AlarmPanelCardConfigState =
 | 
			
		||||
  | "arm_away"
 | 
			
		||||
@@ -345,9 +345,18 @@ export interface LogbookCardConfig extends LovelaceCardConfig {
 | 
			
		||||
  title?: string;
 | 
			
		||||
  hours_to_show?: number;
 | 
			
		||||
  theme?: string;
 | 
			
		||||
  state_filter?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface GeoLocationSourceConfig {
 | 
			
		||||
export interface MapEntityConfig extends EntityConfig {
 | 
			
		||||
  label_mode?: "state" | "attribute" | "name";
 | 
			
		||||
  attribute?: string;
 | 
			
		||||
  unit?: string;
 | 
			
		||||
  focus?: boolean;
 | 
			
		||||
  name?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GeoLocationSourceConfig {
 | 
			
		||||
  source: string;
 | 
			
		||||
  label_mode?: "name" | "state" | "attribute" | "icon";
 | 
			
		||||
  attribute?: string;
 | 
			
		||||
@@ -362,7 +371,7 @@ export interface MapCardConfig extends LovelaceCardConfig {
 | 
			
		||||
  auto_fit?: boolean;
 | 
			
		||||
  fit_zones?: boolean;
 | 
			
		||||
  default_zoom?: number;
 | 
			
		||||
  entities?: (EntityConfig | string)[];
 | 
			
		||||
  entities?: (MapEntityConfig | string)[];
 | 
			
		||||
  hours_to_show?: number;
 | 
			
		||||
  geo_location_sources?: (GeoLocationSourceConfig | string)[];
 | 
			
		||||
  dark_mode?: boolean;
 | 
			
		||||
@@ -434,7 +443,7 @@ export interface StatisticsGraphCardConfig extends EnergyCardBaseConfig {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface StatisticCardConfig extends LovelaceCardConfig {
 | 
			
		||||
  name?: string;
 | 
			
		||||
  name?: string | EntityNameItem | EntityNameItem[];
 | 
			
		||||
  entities: (EntityConfig | string)[];
 | 
			
		||||
  period:
 | 
			
		||||
    | {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import type { HassEntities, HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import { SENSOR_ENTITIES, ASSIST_ENTITIES } from "../../../common/const";
 | 
			
		||||
import { ASSIST_ENTITIES, SENSOR_ENTITIES } from "../../../common/const";
 | 
			
		||||
import { computeDomain } from "../../../common/entity/compute_domain";
 | 
			
		||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
 | 
			
		||||
import { computeStateName } from "../../../common/entity/compute_state_name";
 | 
			
		||||
@@ -14,12 +14,14 @@ import type {
 | 
			
		||||
  GridSourceTypeEnergyPreference,
 | 
			
		||||
} from "../../../data/energy";
 | 
			
		||||
import { domainToName } from "../../../data/integration";
 | 
			
		||||
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
 | 
			
		||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
 | 
			
		||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
 | 
			
		||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
 | 
			
		||||
import { computeUserInitials } from "../../../data/user";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import { HELPER_DOMAINS } from "../../config/helpers/const";
 | 
			
		||||
import type { EntityBadgeConfig } from "../badges/types";
 | 
			
		||||
import type {
 | 
			
		||||
  AlarmPanelCardConfig,
 | 
			
		||||
  EntitiesCardConfig,
 | 
			
		||||
@@ -31,8 +33,7 @@ import type {
 | 
			
		||||
} from "../cards/types";
 | 
			
		||||
import type { EntityConfig } from "../entity-rows/types";
 | 
			
		||||
import type { ButtonsHeaderFooterConfig } from "../header-footer/types";
 | 
			
		||||
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
 | 
			
		||||
import type { EntityBadgeConfig } from "../badges/types";
 | 
			
		||||
import { computeLovelaceEntityName } from "./entity/compute-lovelace-entity-name";
 | 
			
		||||
 | 
			
		||||
const HIDE_DOMAIN = new Set([
 | 
			
		||||
  "ai_task",
 | 
			
		||||
@@ -125,13 +126,13 @@ export const computeSection = (
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const computeCards = (
 | 
			
		||||
  states: HassEntities,
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  entityIds: string[],
 | 
			
		||||
  entityCardOptions: Partial<EntitiesCardConfig>,
 | 
			
		||||
  renderFooterEntities = true
 | 
			
		||||
): LovelaceCardConfig[] => {
 | 
			
		||||
  const cards: LovelaceCardConfig[] = [];
 | 
			
		||||
 | 
			
		||||
  const states = hass.states;
 | 
			
		||||
  // For entity card
 | 
			
		||||
  const entitiesConf: (string | EntityConfig)[] = [];
 | 
			
		||||
 | 
			
		||||
@@ -270,19 +271,23 @@ export const computeCards = (
 | 
			
		||||
        ? states[a]
 | 
			
		||||
          ? computeStateName(states[a])
 | 
			
		||||
          : ""
 | 
			
		||||
        : a.name || "",
 | 
			
		||||
        : states[a.entity]
 | 
			
		||||
          ? computeLovelaceEntityName(hass, states[a.entity], a.name)
 | 
			
		||||
          : "",
 | 
			
		||||
      typeof b === "string"
 | 
			
		||||
        ? states[b]
 | 
			
		||||
          ? computeStateName(states[b])
 | 
			
		||||
          : ""
 | 
			
		||||
        : b.name || ""
 | 
			
		||||
        : states[b.entity]
 | 
			
		||||
          ? computeLovelaceEntityName(hass, states[b.entity], b.name)
 | 
			
		||||
          : ""
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // If we ended up with footer entities but no normal entities,
 | 
			
		||||
  // render the footer entities as normal entities.
 | 
			
		||||
  if (entitiesConf.length === 0 && footerEntities.length > 0) {
 | 
			
		||||
    return computeCards(states, entityIds, entityCardOptions, false);
 | 
			
		||||
    return computeCards(hass, entityIds, entityCardOptions, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (entitiesConf.length > 0 || footerEntities.length > 0) {
 | 
			
		||||
@@ -360,14 +365,14 @@ const computeDefaultViewStates = (
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const generateViewConfig = (
 | 
			
		||||
  localize: LocalizeFunc,
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  path: string,
 | 
			
		||||
  title: string | undefined,
 | 
			
		||||
  icon: string | undefined,
 | 
			
		||||
  entities: HassEntities
 | 
			
		||||
): LovelaceViewConfig => {
 | 
			
		||||
  const ungroupedEntitites: Record<string, string[]> = {};
 | 
			
		||||
 | 
			
		||||
  const { localize } = hass;
 | 
			
		||||
  // Organize ungrouped entities in ungrouped things
 | 
			
		||||
  for (const entityId of Object.keys(entities)) {
 | 
			
		||||
    const state = entities[entityId];
 | 
			
		||||
@@ -470,7 +475,7 @@ export const generateViewConfig = (
 | 
			
		||||
    .forEach((domain) => {
 | 
			
		||||
      cards.push(
 | 
			
		||||
        ...computeCards(
 | 
			
		||||
          entities,
 | 
			
		||||
          hass,
 | 
			
		||||
          ungroupedEntitites[domain].sort((a, b) =>
 | 
			
		||||
            stringCompare(
 | 
			
		||||
              computeStateName(entities[a]),
 | 
			
		||||
@@ -498,16 +503,17 @@ export const generateViewConfig = (
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const generateDefaultViewConfig = (
 | 
			
		||||
  areaEntries: HomeAssistant["areas"],
 | 
			
		||||
  deviceEntries: HomeAssistant["devices"],
 | 
			
		||||
  entityEntries: HomeAssistant["entities"],
 | 
			
		||||
  entities: HassEntities,
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  localize: LocalizeFunc,
 | 
			
		||||
  energyPrefs?: EnergyPreferences,
 | 
			
		||||
  areasPrefs?: AreasDisplayValue,
 | 
			
		||||
  hideEntitiesWithoutAreas?: boolean,
 | 
			
		||||
  hideEnergy?: boolean
 | 
			
		||||
): LovelaceViewConfig => {
 | 
			
		||||
  const entities = hass.states;
 | 
			
		||||
  const areaEntries = hass.areas;
 | 
			
		||||
  const deviceEntries = hass.devices;
 | 
			
		||||
  const entityEntries = hass.entities;
 | 
			
		||||
  const states = computeDefaultViewStates(entities, entityEntries);
 | 
			
		||||
  const path = "default_view";
 | 
			
		||||
  const title = "Home";
 | 
			
		||||
@@ -549,7 +555,7 @@ export const generateDefaultViewConfig = (
 | 
			
		||||
 | 
			
		||||
  for (const groupEntity of splittedByGroups.groups) {
 | 
			
		||||
    groupCards.push(
 | 
			
		||||
      ...computeCards(entities, groupEntity.attributes.entity_id, {
 | 
			
		||||
      ...computeCards(hass, groupEntity.attributes.entity_id, {
 | 
			
		||||
        title: computeStateName(groupEntity),
 | 
			
		||||
        show_header_toggle: groupEntity.attributes.control !== "hidden",
 | 
			
		||||
      })
 | 
			
		||||
@@ -557,7 +563,7 @@ export const generateDefaultViewConfig = (
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const config = generateViewConfig(
 | 
			
		||||
    localize,
 | 
			
		||||
    hass,
 | 
			
		||||
    path,
 | 
			
		||||
    title,
 | 
			
		||||
    icon,
 | 
			
		||||
@@ -575,7 +581,7 @@ export const generateDefaultViewConfig = (
 | 
			
		||||
    const area = areaEntries[areaId];
 | 
			
		||||
    areaCards.push(
 | 
			
		||||
      ...computeCards(
 | 
			
		||||
        entities,
 | 
			
		||||
        hass,
 | 
			
		||||
        areaEntities.map((entity) => entity.entity_id),
 | 
			
		||||
        {
 | 
			
		||||
          title: area.name,
 | 
			
		||||
@@ -601,7 +607,7 @@ export const generateDefaultViewConfig = (
 | 
			
		||||
    const device = deviceEntries[deviceId];
 | 
			
		||||
    deviceCards.push(
 | 
			
		||||
      ...computeCards(
 | 
			
		||||
        entities,
 | 
			
		||||
        hass,
 | 
			
		||||
        deviceEntities.map((entity) => entity.entity_id),
 | 
			
		||||
        {
 | 
			
		||||
          title:
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,12 @@
 | 
			
		||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
 | 
			
		||||
import type { EntityConfig, LovelaceRowConfig } from "../entity-rows/types";
 | 
			
		||||
 | 
			
		||||
interface BaseEntityConfig {
 | 
			
		||||
  type: string;
 | 
			
		||||
  entity: string;
 | 
			
		||||
}
 | 
			
		||||
export const processConfigEntities = <
 | 
			
		||||
  T extends EntityConfig | LovelaceRowConfig,
 | 
			
		||||
  T extends BaseEntityConfig | LovelaceRowConfig,
 | 
			
		||||
>(
 | 
			
		||||
  entities: (T | string)[],
 | 
			
		||||
  checkEntityId = true
 | 
			
		||||
 
 | 
			
		||||
@@ -45,14 +45,13 @@ export class HuiEntityEditor extends LitElement {
 | 
			
		||||
      this.hass.devices
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const name = this.hass.formatEntityName(
 | 
			
		||||
      stateObj,
 | 
			
		||||
      useDeviceName ? { type: "device" } : { type: "entity" }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const isRTL = computeRTL(this.hass);
 | 
			
		||||
 | 
			
		||||
    const primary = item.name || name || item.entity;
 | 
			
		||||
    const primary =
 | 
			
		||||
      this.hass.formatEntityName(
 | 
			
		||||
        stateObj,
 | 
			
		||||
        useDeviceName ? { type: "device" } : { type: "entity" }
 | 
			
		||||
      ) || item.entity;
 | 
			
		||||
 | 
			
		||||
    const secondary = this.hass.formatEntityName(
 | 
			
		||||
      stateObj,
 | 
			
		||||
 
 | 
			
		||||
@@ -4,19 +4,19 @@ import { customElement, property } from "lit/decorators";
 | 
			
		||||
import { classMap } from "lit/directives/class-map";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined";
 | 
			
		||||
import { DOMAINS_INPUT_ROW } from "../../../common/const";
 | 
			
		||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
 | 
			
		||||
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
 | 
			
		||||
import { computeDomain } from "../../../common/entity/compute_domain";
 | 
			
		||||
import { computeStateName } from "../../../common/entity/compute_state_name";
 | 
			
		||||
import "../../../components/entity/state-badge";
 | 
			
		||||
import "../../../components/ha-relative-time";
 | 
			
		||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import type { EntitiesCardEntityConfig } from "../cards/types";
 | 
			
		||||
import { actionHandler } from "../common/directives/action-handler-directive";
 | 
			
		||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
 | 
			
		||||
import { handleAction } from "../common/handle-action";
 | 
			
		||||
import { hasAction, hasAnyAction } from "../common/has-action";
 | 
			
		||||
import { createEntityNotFoundWarning } from "./hui-warning";
 | 
			
		||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
 | 
			
		||||
 | 
			
		||||
@customElement("hui-generic-entity-row")
 | 
			
		||||
export class HuiGenericEntityRow extends LitElement {
 | 
			
		||||
@@ -59,7 +59,11 @@ export class HuiGenericEntityRow extends LitElement {
 | 
			
		||||
    const pointer = hasAnyAction(this.config);
 | 
			
		||||
 | 
			
		||||
    const hasSecondary = this.secondaryText || this.config.secondary_info;
 | 
			
		||||
    const name = this.config.name ?? computeStateName(stateObj);
 | 
			
		||||
    const name = computeLovelaceEntityName(
 | 
			
		||||
      this.hass,
 | 
			
		||||
      stateObj,
 | 
			
		||||
      this.config.name
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <div
 | 
			
		||||
@@ -87,7 +91,7 @@ export class HuiGenericEntityRow extends LitElement {
 | 
			
		||||
              class="info ${classMap({ "text-content": !hasSecondary })}"
 | 
			
		||||
              .title=${name}
 | 
			
		||||
            >
 | 
			
		||||
              ${this.config.name || computeStateName(stateObj)}
 | 
			
		||||
              ${name}
 | 
			
		||||
              ${hasSecondary
 | 
			
		||||
                ? html`
 | 
			
		||||
                    <div class="secondary">
 | 
			
		||||
 
 | 
			
		||||
@@ -296,11 +296,7 @@ export class HuiCreateDialogCard
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _suggestCards(): void {
 | 
			
		||||
    const cardConfig = computeCards(
 | 
			
		||||
      this.hass.states,
 | 
			
		||||
      this._selectedEntities,
 | 
			
		||||
      {}
 | 
			
		||||
    );
 | 
			
		||||
    const cardConfig = computeCards(this.hass, this._selectedEntities, {});
 | 
			
		||||
 | 
			
		||||
    let sectionOptions: Partial<LovelaceSectionConfig> = {};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ import type { HomeAssistant } from "../../../../types";
 | 
			
		||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
 | 
			
		||||
import "../../cards/hui-card";
 | 
			
		||||
import "../../sections/hui-section";
 | 
			
		||||
import { getViewType } from "../../views/get-view-type";
 | 
			
		||||
import { addCards, addSection } from "../config-util";
 | 
			
		||||
import type { LovelaceContainerPath } from "../lovelace-path";
 | 
			
		||||
import { parseLovelaceContainerPath } from "../lovelace-path";
 | 
			
		||||
@@ -66,7 +67,9 @@ export class HuiDialogSuggestCard extends LitElement {
 | 
			
		||||
    const { viewIndex } = parseLovelaceContainerPath(this._params.path);
 | 
			
		||||
    const viewConfig = this._params!.lovelaceConfig.views[viewIndex];
 | 
			
		||||
 | 
			
		||||
    return !isStrategyView(viewConfig) && viewConfig.type === "sections";
 | 
			
		||||
    return (
 | 
			
		||||
      !isStrategyView(viewConfig) && getViewType(viewConfig) === "sections"
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _renderPreview() {
 | 
			
		||||
 
 | 
			
		||||
@@ -46,20 +46,18 @@ export class HuiGenericEntityRowEditor
 | 
			
		||||
    return [
 | 
			
		||||
      { name: "entity", required: true, selector: { entity: {} } },
 | 
			
		||||
      {
 | 
			
		||||
        type: "grid",
 | 
			
		||||
        name: "",
 | 
			
		||||
        schema: [
 | 
			
		||||
          { name: "name", selector: { text: {} } },
 | 
			
		||||
          {
 | 
			
		||||
            name: "icon",
 | 
			
		||||
            selector: {
 | 
			
		||||
              icon: {},
 | 
			
		||||
            },
 | 
			
		||||
            context: {
 | 
			
		||||
              icon_entity: "entity",
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        name: "name",
 | 
			
		||||
        selector: { entity_name: {} },
 | 
			
		||||
        context: { entity: "entity" },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "icon",
 | 
			
		||||
        selector: {
 | 
			
		||||
          icon: {},
 | 
			
		||||
        },
 | 
			
		||||
        context: {
 | 
			
		||||
          icon_entity: "entity",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: "secondary_info",
 | 
			
		||||
 
 | 
			
		||||
@@ -11,20 +11,20 @@ import {
 | 
			
		||||
  string,
 | 
			
		||||
  union,
 | 
			
		||||
} from "superstruct";
 | 
			
		||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
 | 
			
		||||
import { fireEvent } from "../../../../common/dom/fire_event";
 | 
			
		||||
import "../../../../components/ha-form/ha-form";
 | 
			
		||||
import "../hui-sub-element-editor";
 | 
			
		||||
import type { EditDetailElementEvent, SubElementEditorConfig } from "../types";
 | 
			
		||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
 | 
			
		||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
 | 
			
		||||
import type { HomeAssistant } from "../../../../types";
 | 
			
		||||
import type { ConfigEntity, GlanceCardConfig } from "../../cards/types";
 | 
			
		||||
import "../../components/hui-entity-editor";
 | 
			
		||||
import type { EntityConfig } from "../../entity-rows/types";
 | 
			
		||||
import type { LovelaceCardEditor } from "../../types";
 | 
			
		||||
import "../hui-sub-element-editor";
 | 
			
		||||
import { processEditorEntities } from "../process-editor-entities";
 | 
			
		||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
 | 
			
		||||
import { entitiesConfigStruct } from "../structs/entities-struct";
 | 
			
		||||
import type { EntityConfig } from "../../entity-rows/types";
 | 
			
		||||
import type { EditDetailElementEvent, SubElementEditorConfig } from "../types";
 | 
			
		||||
 | 
			
		||||
const cardConfigStruct = assign(
 | 
			
		||||
  baseLovelaceCardConfig,
 | 
			
		||||
@@ -42,11 +42,17 @@ const cardConfigStruct = assign(
 | 
			
		||||
 | 
			
		||||
const SUB_SCHEMA = [
 | 
			
		||||
  { name: "entity", selector: { entity: {} }, required: true },
 | 
			
		||||
  {
 | 
			
		||||
    name: "name",
 | 
			
		||||
    selector: { entity_name: {} },
 | 
			
		||||
    context: {
 | 
			
		||||
      entity: "entity",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: "grid",
 | 
			
		||||
    name: "",
 | 
			
		||||
    schema: [
 | 
			
		||||
      { name: "name", selector: { text: {} } },
 | 
			
		||||
      {
 | 
			
		||||
        name: "icon",
 | 
			
		||||
        selector: {
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,13 @@ const cardConfigStruct = assign(
 | 
			
		||||
 | 
			
		||||
const SUB_SCHEMA = [
 | 
			
		||||
  { name: "entity", selector: { entity: {} }, required: true },
 | 
			
		||||
  { name: "name", selector: { text: {} } },
 | 
			
		||||
  {
 | 
			
		||||
    name: "name",
 | 
			
		||||
    selector: { entity_name: {} },
 | 
			
		||||
    context: {
 | 
			
		||||
      entity: "entity",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
] as const;
 | 
			
		||||
 | 
			
		||||
@customElement("hui-history-graph-card-editor")
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import {
 | 
			
		||||
  string,
 | 
			
		||||
} from "superstruct";
 | 
			
		||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { fireEvent } from "../../../../common/dom/fire_event";
 | 
			
		||||
import "../../../../components/entity/ha-entities-picker";
 | 
			
		||||
import "../../../../components/ha-target-picker";
 | 
			
		||||
@@ -24,6 +25,7 @@ import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-logbook-card";
 | 
			
		||||
import { targetStruct } from "../../../../data/script";
 | 
			
		||||
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
 | 
			
		||||
import type { HaEntityPickerEntityFilterFunc } from "../../../../data/entity";
 | 
			
		||||
import { resolveEntityIDs } from "../../../../data/selector";
 | 
			
		||||
 | 
			
		||||
const cardConfigStruct = assign(
 | 
			
		||||
  baseLovelaceCardConfig,
 | 
			
		||||
@@ -33,6 +35,7 @@ const cardConfigStruct = assign(
 | 
			
		||||
    hours_to_show: optional(number()),
 | 
			
		||||
    theme: optional(string()),
 | 
			
		||||
    target: optional(targetStruct),
 | 
			
		||||
    state_filter: optional(array(string())),
 | 
			
		||||
  })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
@@ -50,6 +53,13 @@ const SCHEMA = [
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: "state_filter",
 | 
			
		||||
    context: {
 | 
			
		||||
      filter_entity: "context_entities",
 | 
			
		||||
    },
 | 
			
		||||
    selector: { state: { multiple: true } },
 | 
			
		||||
  },
 | 
			
		||||
] as const;
 | 
			
		||||
 | 
			
		||||
@customElement("hui-logbook-card-editor")
 | 
			
		||||
@@ -106,7 +116,13 @@ export class HuiLogbookCardEditor
 | 
			
		||||
    return html`
 | 
			
		||||
      <ha-form
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        .data=${this._config}
 | 
			
		||||
        .data=${this._data(
 | 
			
		||||
          this._config,
 | 
			
		||||
          this._targetPicker,
 | 
			
		||||
          this.hass.entities,
 | 
			
		||||
          this.hass.devices,
 | 
			
		||||
          this.hass.areas
 | 
			
		||||
        )}
 | 
			
		||||
        .schema=${SCHEMA}
 | 
			
		||||
        .computeLabel=${this._computeLabelCallback}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
@@ -122,6 +138,25 @@ export class HuiLogbookCardEditor
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _data = memoizeOne(
 | 
			
		||||
    (
 | 
			
		||||
      config: LogbookCardConfig,
 | 
			
		||||
      target: HassServiceTarget,
 | 
			
		||||
      entities: HomeAssistant["entities"],
 | 
			
		||||
      devices: HomeAssistant["devices"],
 | 
			
		||||
      areas: HomeAssistant["areas"]
 | 
			
		||||
    ) => ({
 | 
			
		||||
      ...config,
 | 
			
		||||
      context_entities: resolveEntityIDs(
 | 
			
		||||
        this.hass!,
 | 
			
		||||
        target,
 | 
			
		||||
        entities,
 | 
			
		||||
        devices,
 | 
			
		||||
        areas
 | 
			
		||||
      ),
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  private _filterFunc: HaEntityPickerEntityFilterFunc = (entity) =>
 | 
			
		||||
    filterLogbookCompatibleEntities(entity, this._sensorNumericDeviceClasses);
 | 
			
		||||
 | 
			
		||||
@@ -131,7 +166,9 @@ export class HuiLogbookCardEditor
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _valueChanged(ev: CustomEvent): void {
 | 
			
		||||
    fireEvent(this, "config-changed", { config: ev.detail.value });
 | 
			
		||||
    const newConfig = { ...ev.detail.value };
 | 
			
		||||
    delete newConfig.context_entities;
 | 
			
		||||
    fireEvent(this, "config-changed", { config: newConfig });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
 | 
			
		||||
@@ -142,6 +179,10 @@ export class HuiLogbookCardEditor
 | 
			
		||||
        )} (${this.hass!.localize(
 | 
			
		||||
          "ui.panel.lovelace.editor.card.config.optional"
 | 
			
		||||
        )})`;
 | 
			
		||||
      case "state_filter":
 | 
			
		||||
        return this.hass!.localize(
 | 
			
		||||
          "ui.panel.lovelace.editor.card.logbook.state_filter"
 | 
			
		||||
        );
 | 
			
		||||
      default:
 | 
			
		||||
        return this.hass!.localize(
 | 
			
		||||
          `ui.panel.lovelace.editor.card.generic.${schema.name}`
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import { mdiPalette } from "@mdi/js";
 | 
			
		||||
import type { CSSResultGroup } from "lit";
 | 
			
		||||
import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import {
 | 
			
		||||
  array,
 | 
			
		||||
  assert,
 | 
			
		||||
@@ -13,19 +14,19 @@ import {
 | 
			
		||||
  string,
 | 
			
		||||
  union,
 | 
			
		||||
} from "superstruct";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { fireEvent } from "../../../../common/dom/fire_event";
 | 
			
		||||
import { hasLocation } from "../../../../common/entity/has_location";
 | 
			
		||||
import { computeDomain } from "../../../../common/entity/compute_domain";
 | 
			
		||||
import { hasLocation } from "../../../../common/entity/has_location";
 | 
			
		||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
 | 
			
		||||
import "../../../../components/ha-form/ha-form";
 | 
			
		||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
 | 
			
		||||
import type { SelectSelector } from "../../../../data/selector";
 | 
			
		||||
import "../../../../components/ha-formfield";
 | 
			
		||||
import "../../../../components/ha-switch";
 | 
			
		||||
import "../../../../components/ha-selector/ha-selector-select";
 | 
			
		||||
import "../../../../components/ha-switch";
 | 
			
		||||
import type { SelectSelector } from "../../../../data/selector";
 | 
			
		||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
 | 
			
		||||
import { DEFAULT_HOURS_TO_SHOW, DEFAULT_ZOOM } from "../../cards/hui-map-card";
 | 
			
		||||
import type { MapCardConfig } from "../../cards/types";
 | 
			
		||||
import type { MapCardConfig, MapEntityConfig } from "../../cards/types";
 | 
			
		||||
import "../../components/hui-entity-editor";
 | 
			
		||||
import type { EntityConfig } from "../../entity-rows/types";
 | 
			
		||||
import type { LovelaceCardEditor } from "../../types";
 | 
			
		||||
@@ -33,7 +34,6 @@ import { processEditorEntities } from "../process-editor-entities";
 | 
			
		||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
 | 
			
		||||
import type { EntitiesEditorEvent } from "../types";
 | 
			
		||||
import { configElementStyle } from "./config-elements-style";
 | 
			
		||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
 | 
			
		||||
 | 
			
		||||
export const mapEntitiesConfigStruct = union([
 | 
			
		||||
  object({
 | 
			
		||||
@@ -223,7 +223,9 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  private _entitiesValueChanged(ev: EntitiesEditorEvent): void {
 | 
			
		||||
  private _entitiesValueChanged(
 | 
			
		||||
    ev: EntitiesEditorEvent<MapEntityConfig>
 | 
			
		||||
  ): void {
 | 
			
		||||
    if (ev.detail && ev.detail.entities) {
 | 
			
		||||
      this._config = { ...this._config!, entities: ev.detail.entities };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,12 +21,13 @@ import type { StatisticCardConfig } from "../../cards/types";
 | 
			
		||||
import { headerFooterConfigStructs } from "../../header-footer/structs";
 | 
			
		||||
import type { LovelaceCardEditor } from "../../types";
 | 
			
		||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
 | 
			
		||||
import { entityNameStruct } from "../structs/entity-name-struct";
 | 
			
		||||
 | 
			
		||||
const cardConfigStruct = assign(
 | 
			
		||||
  baseLovelaceCardConfig,
 | 
			
		||||
  object({
 | 
			
		||||
    entity: optional(string()),
 | 
			
		||||
    name: optional(string()),
 | 
			
		||||
    name: optional(entityNameStruct),
 | 
			
		||||
    icon: optional(string()),
 | 
			
		||||
    unit: optional(string()),
 | 
			
		||||
    stat_type: optional(string()),
 | 
			
		||||
@@ -144,11 +145,15 @@ export class HuiStatisticCardEditor
 | 
			
		||||
                }
 | 
			
		||||
              : { object: {} },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: "name",
 | 
			
		||||
          selector: { entity_name: {} },
 | 
			
		||||
          context: { entity: "entity" },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: "grid",
 | 
			
		||||
          name: "",
 | 
			
		||||
          schema: [
 | 
			
		||||
            { name: "name", selector: { text: {} } },
 | 
			
		||||
            {
 | 
			
		||||
              name: "icon",
 | 
			
		||||
              selector: {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,11 +4,12 @@ import {
 | 
			
		||||
  actionConfigStruct,
 | 
			
		||||
  actionConfigStructConfirmation,
 | 
			
		||||
} from "./action-struct";
 | 
			
		||||
import { entityNameStruct } from "./entity-name-struct";
 | 
			
		||||
 | 
			
		||||
export const entitiesConfigStruct = union([
 | 
			
		||||
  object({
 | 
			
		||||
    entity: string(),
 | 
			
		||||
    name: optional(string()),
 | 
			
		||||
    name: optional(entityNameStruct),
 | 
			
		||||
    icon: optional(string()),
 | 
			
		||||
    image: optional(string()),
 | 
			
		||||
    secondary_info: optional(string()),
 | 
			
		||||
 
 | 
			
		||||
@@ -43,9 +43,10 @@ export interface ConfigError {
 | 
			
		||||
  message: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EntitiesEditorEvent extends CustomEvent {
 | 
			
		||||
export interface EntitiesEditorEvent<T extends EntityConfig = EntityConfig>
 | 
			
		||||
  extends CustomEvent {
 | 
			
		||||
  detail: {
 | 
			
		||||
    entities?: EntityConfig[];
 | 
			
		||||
    entities?: T[];
 | 
			
		||||
    item?: any;
 | 
			
		||||
  };
 | 
			
		||||
  target: EventTarget | null;
 | 
			
		||||
 
 | 
			
		||||
@@ -111,11 +111,7 @@ export class HuiUnusedEntities extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _addToLovelaceView(): void {
 | 
			
		||||
    const cardConfig = computeCards(
 | 
			
		||||
      this.hass.states,
 | 
			
		||||
      this._selectedEntities,
 | 
			
		||||
      {}
 | 
			
		||||
    );
 | 
			
		||||
    const cardConfig = computeCards(this.hass, this._selectedEntities, {});
 | 
			
		||||
    const sectionConfig = computeSection(this._selectedEntities, {});
 | 
			
		||||
 | 
			
		||||
    if (this.lovelace.config.views.length === 1) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,17 @@
 | 
			
		||||
import { format } from "date-fns";
 | 
			
		||||
import type { PropertyValues, TemplateResult } from "lit";
 | 
			
		||||
import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
import "../../../components/ha-date-input";
 | 
			
		||||
import { format } from "date-fns";
 | 
			
		||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity";
 | 
			
		||||
import "../../../components/ha-time-input";
 | 
			
		||||
import { setDateTimeValue } from "../../../data/datetime";
 | 
			
		||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
 | 
			
		||||
import "../components/hui-generic-entity-row";
 | 
			
		||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
 | 
			
		||||
import type { EntityConfig, LovelaceRow } from "./types";
 | 
			
		||||
import "../../../components/ha-time-input";
 | 
			
		||||
import { computeStateName } from "../../../common/entity/compute_state_name";
 | 
			
		||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
 | 
			
		||||
 | 
			
		||||
@customElement("hui-datetime-entity-row")
 | 
			
		||||
class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
@@ -53,6 +53,12 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
    const time = dateObj ? format(dateObj, "HH:mm:ss") : undefined;
 | 
			
		||||
    const date = dateObj ? format(dateObj, "yyyy-MM-dd") : undefined;
 | 
			
		||||
 | 
			
		||||
    const name = computeLovelaceEntityName(
 | 
			
		||||
      this.hass!,
 | 
			
		||||
      stateObj,
 | 
			
		||||
      this._config.name
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <hui-generic-entity-row
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
@@ -61,7 +67,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
      >
 | 
			
		||||
        <div>
 | 
			
		||||
          <ha-date-input
 | 
			
		||||
            .label=${this._config.name || computeStateName(stateObj)}
 | 
			
		||||
            .label=${name}
 | 
			
		||||
            .locale=${this.hass.locale}
 | 
			
		||||
            .value=${date}
 | 
			
		||||
            .disabled=${unavailable}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import type { PropertyValues } from "lit";
 | 
			
		||||
import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
import { computeStateName } from "../../../common/entity/compute_state_name";
 | 
			
		||||
import "../../../components/ha-date-input";
 | 
			
		||||
import "../../../components/ha-time-input";
 | 
			
		||||
import { isUnavailableState, UNKNOWN } from "../../../data/entity";
 | 
			
		||||
@@ -10,6 +9,7 @@ import {
 | 
			
		||||
  stateToIsoDateString,
 | 
			
		||||
} from "../../../data/input_datetime";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
 | 
			
		||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
 | 
			
		||||
import "../components/hui-generic-entity-row";
 | 
			
		||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
 | 
			
		||||
@@ -47,7 +47,11 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
      `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const name = this._config.name || computeStateName(stateObj);
 | 
			
		||||
    const name = computeLovelaceEntityName(
 | 
			
		||||
      this.hass!,
 | 
			
		||||
      stateObj,
 | 
			
		||||
      this._config.name
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <hui-generic-entity-row
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ import type { PropertyValues } from "lit";
 | 
			
		||||
import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
 | 
			
		||||
import { computeStateName } from "../../../common/entity/compute_state_name";
 | 
			
		||||
import "../../../components/ha-list-item";
 | 
			
		||||
import "../../../components/ha-select";
 | 
			
		||||
import { UNAVAILABLE } from "../../../data/entity";
 | 
			
		||||
@@ -11,6 +10,7 @@ import type { InputSelectEntity } from "../../../data/input_select";
 | 
			
		||||
import { setInputSelectOption } from "../../../data/input_select";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import type { EntitiesCardEntityConfig } from "../cards/types";
 | 
			
		||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
 | 
			
		||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
 | 
			
		||||
import "../components/hui-generic-entity-row";
 | 
			
		||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
 | 
			
		||||
@@ -51,6 +51,12 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
      `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const name = computeLovelaceEntityName(
 | 
			
		||||
      this.hass!,
 | 
			
		||||
      stateObj,
 | 
			
		||||
      this._config.name
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <hui-generic-entity-row
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
@@ -58,7 +64,7 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
        hide-name
 | 
			
		||||
      >
 | 
			
		||||
        <ha-select
 | 
			
		||||
          .label=${this._config.name || computeStateName(stateObj)}
 | 
			
		||||
          .label=${name}
 | 
			
		||||
          .value=${stateObj.state}
 | 
			
		||||
          .options=${stateObj.attributes.options}
 | 
			
		||||
          .disabled=${
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import type { PropertyValues } from "lit";
 | 
			
		||||
import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
import { computeStateName } from "../../../common/entity/compute_state_name";
 | 
			
		||||
import "../../../components/ha-textfield";
 | 
			
		||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity";
 | 
			
		||||
import { setValue } from "../../../data/input_text";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
 | 
			
		||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
 | 
			
		||||
import "../components/hui-generic-entity-row";
 | 
			
		||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
 | 
			
		||||
@@ -43,6 +43,12 @@ class HuiInputTextEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
      `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const name = computeLovelaceEntityName(
 | 
			
		||||
      this.hass!,
 | 
			
		||||
      stateObj,
 | 
			
		||||
      this._config.name
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <hui-generic-entity-row
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
@@ -50,7 +56,7 @@ class HuiInputTextEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
        hide-name
 | 
			
		||||
      >
 | 
			
		||||
        <ha-textfield
 | 
			
		||||
          .label=${this._config.name || computeStateName(stateObj)}
 | 
			
		||||
          .label=${name}
 | 
			
		||||
          .disabled=${stateObj.state === UNAVAILABLE}
 | 
			
		||||
          .value=${stateObj.state}
 | 
			
		||||
          .minlength=${stateObj.attributes.min}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ import type { PropertyValues } from "lit";
 | 
			
		||||
import { LitElement, css, html, nothing } from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
 | 
			
		||||
import { computeStateName } from "../../../common/entity/compute_state_name";
 | 
			
		||||
import "../../../components/ha-list-item";
 | 
			
		||||
import "../../../components/ha-select";
 | 
			
		||||
import { UNAVAILABLE } from "../../../data/entity";
 | 
			
		||||
@@ -11,6 +10,7 @@ import type { SelectEntity } from "../../../data/select";
 | 
			
		||||
import { setSelectOption } from "../../../data/select";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import type { EntitiesCardEntityConfig } from "../cards/types";
 | 
			
		||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
 | 
			
		||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
 | 
			
		||||
import "../components/hui-generic-entity-row";
 | 
			
		||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
 | 
			
		||||
@@ -51,6 +51,12 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
      `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const name = computeLovelaceEntityName(
 | 
			
		||||
      this.hass!,
 | 
			
		||||
      stateObj,
 | 
			
		||||
      this._config.name
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <hui-generic-entity-row
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
@@ -58,7 +64,7 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
        hide-name
 | 
			
		||||
      >
 | 
			
		||||
        <ha-select
 | 
			
		||||
          .label=${this._config.name || computeStateName(stateObj)}
 | 
			
		||||
          .label=${name}
 | 
			
		||||
          .value=${stateObj.state}
 | 
			
		||||
          .options=${stateObj.attributes.options}
 | 
			
		||||
          .disabled=${stateObj.state === UNAVAILABLE}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import type { PropertyValues } from "lit";
 | 
			
		||||
import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
import { computeStateName } from "../../../common/entity/compute_state_name";
 | 
			
		||||
import "../../../components/ha-textfield";
 | 
			
		||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity";
 | 
			
		||||
import type { TextEntity } from "../../../data/text";
 | 
			
		||||
@@ -11,6 +10,7 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
 | 
			
		||||
import "../components/hui-generic-entity-row";
 | 
			
		||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
 | 
			
		||||
import type { EntityConfig, LovelaceRow } from "./types";
 | 
			
		||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
 | 
			
		||||
 | 
			
		||||
@customElement("hui-text-entity-row")
 | 
			
		||||
class HuiTextEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
@@ -46,6 +46,12 @@ class HuiTextEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
      `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const name = computeLovelaceEntityName(
 | 
			
		||||
      this.hass!,
 | 
			
		||||
      stateObj,
 | 
			
		||||
      this._config.name
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <hui-generic-entity-row
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
@@ -53,7 +59,7 @@ class HuiTextEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
        hide-name
 | 
			
		||||
      >
 | 
			
		||||
        <ha-textfield
 | 
			
		||||
          .label=${this._config.name || computeStateName(stateObj)}
 | 
			
		||||
          .label=${name}
 | 
			
		||||
          .disabled=${stateObj.state === UNAVAILABLE}
 | 
			
		||||
          .value=${stateObj.state}
 | 
			
		||||
          .minlength=${stateObj.attributes.min}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@ import { LitElement, css, html, nothing } from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
import { classMap } from "lit/directives/class-map";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined";
 | 
			
		||||
import { computeStateName } from "../../../common/entity/compute_state_name";
 | 
			
		||||
import { isUnavailableState } from "../../../data/entity";
 | 
			
		||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
 | 
			
		||||
import type { ForecastEvent, WeatherEntity } from "../../../data/weather";
 | 
			
		||||
@@ -24,6 +23,7 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
 | 
			
		||||
import "../components/hui-generic-entity-row";
 | 
			
		||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
 | 
			
		||||
import type { LovelaceRow } from "./types";
 | 
			
		||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
 | 
			
		||||
 | 
			
		||||
@customElement("hui-weather-entity-row")
 | 
			
		||||
class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
@@ -119,6 +119,12 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
    const forecastData = getForecast(stateObj.attributes, this._forecastEvent);
 | 
			
		||||
    const forecast = forecastData?.forecast;
 | 
			
		||||
 | 
			
		||||
    const name = computeLovelaceEntityName(
 | 
			
		||||
      this.hass!,
 | 
			
		||||
      stateObj,
 | 
			
		||||
      this._config.name
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <div
 | 
			
		||||
        class="icon-image ${classMap({
 | 
			
		||||
@@ -155,7 +161,7 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
 | 
			
		||||
          hasDoubleClick: hasAction(this._config!.double_tap_action),
 | 
			
		||||
        })}
 | 
			
		||||
      >
 | 
			
		||||
        ${this._config.name || computeStateName(stateObj)}
 | 
			
		||||
        ${name}
 | 
			
		||||
        ${hasSecondary
 | 
			
		||||
          ? html`
 | 
			
		||||
              <div class="secondary">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display";
 | 
			
		||||
import type {
 | 
			
		||||
  ActionConfig,
 | 
			
		||||
  ConfirmationRestrictionConfig,
 | 
			
		||||
@@ -10,7 +11,7 @@ import type { TimestampRenderingFormat } from "../components/types";
 | 
			
		||||
export interface EntityConfig {
 | 
			
		||||
  entity: string;
 | 
			
		||||
  type?: string;
 | 
			
		||||
  name?: string;
 | 
			
		||||
  name?: string | EntityNameItem | EntityNameItem[];
 | 
			
		||||
  icon?: string;
 | 
			
		||||
  image?: string;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -321,6 +321,8 @@ class HUIRoot extends LitElement {
 | 
			
		||||
                .id="button-${index}"
 | 
			
		||||
                .path=${item.icon}
 | 
			
		||||
                slot="trigger"
 | 
			
		||||
                .label=${label}
 | 
			
		||||
                hide-title
 | 
			
		||||
              ></ha-icon-button>
 | 
			
		||||
              ${item.subItems
 | 
			
		||||
                .filter((subItem) => subItem.visible)
 | 
			
		||||
@@ -340,9 +342,6 @@ class HUIRoot extends LitElement {
 | 
			
		||||
                  `
 | 
			
		||||
                )}
 | 
			
		||||
            </ha-button-menu>
 | 
			
		||||
            <ha-tooltip placement="bottom" .for="button-${index}">
 | 
			
		||||
              ${label}
 | 
			
		||||
            </ha-tooltip>
 | 
			
		||||
          `
 | 
			
		||||
        : html`
 | 
			
		||||
            <ha-icon-button
 | 
			
		||||
 
 | 
			
		||||
@@ -2,15 +2,15 @@ import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, state } from "lit/decorators";
 | 
			
		||||
import { DOMAINS_TOGGLE } from "../../../common/const";
 | 
			
		||||
import { computeDomain } from "../../../common/entity/compute_domain";
 | 
			
		||||
import { computeStateName } from "../../../common/entity/compute_state_name";
 | 
			
		||||
import "../../../components/ha-state-icon";
 | 
			
		||||
import "../../../components/ha-button";
 | 
			
		||||
import "../../../components/ha-state-icon";
 | 
			
		||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import { actionHandler } from "../common/directives/action-handler-directive";
 | 
			
		||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
 | 
			
		||||
import { handleAction } from "../common/handle-action";
 | 
			
		||||
import { hasAction } from "../common/has-action";
 | 
			
		||||
import type { ButtonRowConfig, LovelaceRow } from "../entity-rows/types";
 | 
			
		||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
 | 
			
		||||
 | 
			
		||||
@customElement("hui-button-row")
 | 
			
		||||
export class HuiButtonRow extends LitElement implements LovelaceRow {
 | 
			
		||||
@@ -49,8 +49,11 @@ export class HuiButtonRow extends LitElement implements LovelaceRow {
 | 
			
		||||
        ? this.hass.states[this._config.entity]
 | 
			
		||||
        : undefined;
 | 
			
		||||
 | 
			
		||||
    const name =
 | 
			
		||||
      this._config.name ?? (stateObj ? computeStateName(stateObj) : "");
 | 
			
		||||
    const name = computeLovelaceEntityName(
 | 
			
		||||
      this.hass!,
 | 
			
		||||
      stateObj,
 | 
			
		||||
      this._config.name
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <ha-state-icon
 | 
			
		||||
 
 | 
			
		||||
@@ -44,10 +44,7 @@ export class OriginalStatesViewStrategy extends ReactiveElement {
 | 
			
		||||
    // User can override default view. If they didn't, we will add one
 | 
			
		||||
    // that contains all entities.
 | 
			
		||||
    const view = generateDefaultViewConfig(
 | 
			
		||||
      hass.areas,
 | 
			
		||||
      hass.devices,
 | 
			
		||||
      hass.entities,
 | 
			
		||||
      hass.states,
 | 
			
		||||
      hass,
 | 
			
		||||
      localize,
 | 
			
		||||
      energyPrefs,
 | 
			
		||||
      config.areas,
 | 
			
		||||
 
 | 
			
		||||
@@ -824,6 +824,7 @@
 | 
			
		||||
        "add_new": "Add new area…",
 | 
			
		||||
        "no_areas": "You don't have any areas",
 | 
			
		||||
        "no_match": "No matching areas found",
 | 
			
		||||
        "placeholder": "Select an area",
 | 
			
		||||
        "unassigned_areas": "Unassigned areas",
 | 
			
		||||
        "failed_create_area": "Failed to create area."
 | 
			
		||||
      },
 | 
			
		||||
@@ -1706,6 +1707,14 @@
 | 
			
		||||
        "update": "[%key:ui::panel::config::devices::update%]",
 | 
			
		||||
        "unknown_error": "[%key:ui::panel::config::devices::unknown_error%]"
 | 
			
		||||
      },
 | 
			
		||||
      "label-detail": {
 | 
			
		||||
        "new_label": "New label",
 | 
			
		||||
        "name": "Name",
 | 
			
		||||
        "icon": "Icon",
 | 
			
		||||
        "color": "Color",
 | 
			
		||||
        "description": "Description",
 | 
			
		||||
        "required_error_msg": "[%key:ui::panel::config::zone::detail::required_error_msg%]"
 | 
			
		||||
      },
 | 
			
		||||
      "voice-settings": {
 | 
			
		||||
        "expose_header": "Expose",
 | 
			
		||||
        "aliases_header": "Aliases",
 | 
			
		||||
@@ -2406,18 +2415,7 @@
 | 
			
		||||
          "introduction": "Labels can help you organize your areas, devices, and entities. They can be used to filter in the UI, or use them as a target in automations.",
 | 
			
		||||
          "introduction2": "Go to the area, device, or entity you want to add a label to, and press the edit button to assign labels to them.",
 | 
			
		||||
          "confirm_remove_title": "Remove label?",
 | 
			
		||||
          "confirm_remove": "Are you sure you want to remove label {label}? It will be removed from all areas, devices, and entities.",
 | 
			
		||||
          "detail": {
 | 
			
		||||
            "new_label": "New label",
 | 
			
		||||
            "name": "Name",
 | 
			
		||||
            "icon": "Icon",
 | 
			
		||||
            "color": "Color",
 | 
			
		||||
            "description": "Description",
 | 
			
		||||
            "delete": "Delete",
 | 
			
		||||
            "update": "Update",
 | 
			
		||||
            "create": "Create",
 | 
			
		||||
            "required_error_msg": "[%key:ui::panel::config::zone::detail::required_error_msg%]"
 | 
			
		||||
          }
 | 
			
		||||
          "confirm_remove": "Are you sure you want to remove label {label}? It will be removed from all areas, devices, and entities."
 | 
			
		||||
        },
 | 
			
		||||
        "areas": {
 | 
			
		||||
          "caption": "Areas",
 | 
			
		||||
@@ -3629,6 +3627,36 @@
 | 
			
		||||
            "older_run": "Older run",
 | 
			
		||||
            "newer_run": "Newer run",
 | 
			
		||||
            "start_debug_run": "Start debug run",
 | 
			
		||||
            "error": {
 | 
			
		||||
              "code": "Error code",
 | 
			
		||||
              "fetch_events": "Failed to fetch events",
 | 
			
		||||
              "fetch_runs": "Failed to fetch pipeline runs",
 | 
			
		||||
              "playing_audio": "Error playing audio",
 | 
			
		||||
              "showing_run": "Error showing run",
 | 
			
		||||
              "title": "Error"
 | 
			
		||||
            },
 | 
			
		||||
            "no_events": "There were no events in this run.",
 | 
			
		||||
            "play_audio": "Play audio",
 | 
			
		||||
            "raw": "Raw",
 | 
			
		||||
            "run": "Run",
 | 
			
		||||
            "stages": {
 | 
			
		||||
              "engine": "Engine",
 | 
			
		||||
              "input": "Input",
 | 
			
		||||
              "language": "Language",
 | 
			
		||||
              "model": "Model",
 | 
			
		||||
              "natural_language_processing": "Natural language processing",
 | 
			
		||||
              "output": "Output",
 | 
			
		||||
              "pipeline": "Pipeline",
 | 
			
		||||
              "prefer_local": "Prefer handling locally",
 | 
			
		||||
              "processed_locally": "Processed locally",
 | 
			
		||||
              "response_type": "Response type",
 | 
			
		||||
              "speech_to_text": "Speech-to-text",
 | 
			
		||||
              "text_to_speech": "Text-to-speech",
 | 
			
		||||
              "timestamp": "Timestamp",
 | 
			
		||||
              "voice": "Voice",
 | 
			
		||||
              "wake_word": "Wake word"
 | 
			
		||||
            },
 | 
			
		||||
            "stop_audio": "Stop audio",
 | 
			
		||||
            "pipeline": {
 | 
			
		||||
              "header": "Assist pipeline",
 | 
			
		||||
              "run_text_pipeline": "Run text pipeline",
 | 
			
		||||
@@ -3945,7 +3973,6 @@
 | 
			
		||||
              "add": "Add trigger",
 | 
			
		||||
              "empty_search": "No triggers found for {term}",
 | 
			
		||||
              "id": "Trigger ID",
 | 
			
		||||
              "id_helper": "Helps identify each run based on which trigger fired.",
 | 
			
		||||
              "optional": "Optional",
 | 
			
		||||
              "edit_id": "Edit ID",
 | 
			
		||||
              "duplicate": "[%key:ui::common::duplicate%]",
 | 
			
		||||
@@ -7089,6 +7116,11 @@
 | 
			
		||||
              "card_indicates_energy_used": "This card indicates how much of the electricity consumed by your home was generated using non-fossil fuels like solar, wind, and nuclear. The higher, the better!",
 | 
			
		||||
              "low_carbon_energy_consumed": "Low-carbon electricity consumed",
 | 
			
		||||
              "low_carbon_energy_not_calculated": "Consumed low-carbon electricity couldn't be calculated"
 | 
			
		||||
            },
 | 
			
		||||
            "energy_compare": {
 | 
			
		||||
              "info": "You are comparing the period {start} with the period {end}",
 | 
			
		||||
              "compare_previous_year": "Compare previous year",
 | 
			
		||||
              "compare_previous_period": "Compare previous period"
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          "heading": {
 | 
			
		||||
@@ -7652,7 +7684,8 @@
 | 
			
		||||
            },
 | 
			
		||||
            "logbook": {
 | 
			
		||||
              "name": "Activity",
 | 
			
		||||
              "description": "The Activity card shows a list of events for entities."
 | 
			
		||||
              "description": "The Activity card shows a list of events for entities.",
 | 
			
		||||
              "state_filter": "State filter"
 | 
			
		||||
            },
 | 
			
		||||
            "history-graph": {
 | 
			
		||||
              "name": "History graph",
 | 
			
		||||
@@ -9336,11 +9369,6 @@
 | 
			
		||||
      "energy": {
 | 
			
		||||
        "download_data": "[%key:ui::panel::history::download_data%]",
 | 
			
		||||
        "configure": "[%key:ui::dialogs::quick-bar::commands::navigation::energy%]",
 | 
			
		||||
        "compare": {
 | 
			
		||||
          "info": "You are comparing the period {start} with the period {end}",
 | 
			
		||||
          "compare_previous_year": "Compare previous year",
 | 
			
		||||
          "compare_previous_period": "Compare previous period"
 | 
			
		||||
        },
 | 
			
		||||
        "setup": {
 | 
			
		||||
          "next": "Next",
 | 
			
		||||
          "back": "Back",
 | 
			
		||||
 
 | 
			
		||||
@@ -201,6 +201,7 @@ describe("getStates", () => {
 | 
			
		||||
          "pm1",
 | 
			
		||||
          "pm10",
 | 
			
		||||
          "pm25",
 | 
			
		||||
          "pm4",
 | 
			
		||||
          "power_factor",
 | 
			
		||||
          "power",
 | 
			
		||||
          "pressure",
 | 
			
		||||
@@ -215,7 +216,7 @@ describe("getStates", () => {
 | 
			
		||||
          "volume_flow_rate",
 | 
			
		||||
        ])
 | 
			
		||||
      );
 | 
			
		||||
      expect(result.length).toBe(34);
 | 
			
		||||
      expect(result.length).toBe(35);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should return empty array for unknown attribute", () => {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user