Compare commits

..

25 Commits

Author SHA1 Message Date
Paul Bottein
36f5e50ccc Display label inside field for entity picker and area picker 2025-11-03 17:07:22 +01:00
Ezra Freedman
896d76b218 Fix calendar all-day toggle date normalization (#27701) 2025-11-03 14:55:20 +02:00
Petar Petrov
cec24117dc Auto update statistics graph in more-info (#27760) 2025-11-03 14:02:15 +02:00
Aidan Timson
34006d268b Translate voice assistant pipeline debugger (#27721)
* Translate voice assistant pipeline debugger

* Update src/panels/config/voice-assistants/debug/assist-render-pipeline-run.ts

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

* Typo

* Use keys in render functions

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2025-11-03 11:47:38 +01:00
renovate[bot]
54c03d91df Update dependency @rsdoctor/rspack-plugin to v1.3.7 (#27761)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 11:08:23 +02:00
Paul Bottein
52a56a1c4e Fix suggest cards dialog for sections view (#27762) 2025-11-03 09:56:52 +01:00
Copilot
e49feeb4aa Show action description in sidebar header using describeAction (#27516)
Co-authored-by: MindFreeze <5219205+MindFreeze@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2025-11-03 08:58:57 +01:00
Simon Lamon
a0c30e433a Move label translations to ui.dialog (#27752) 2025-11-03 08:20:12 +02:00
renovate[bot]
354ce027eb Update dependency jsdom to v27.1.0 (#27759)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 08:15:46 +02:00
dependabot[bot]
5c224a942d Bump github/codeql-action from 4.31.0 to 4.31.2 (#27758)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.0 to 4.31.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](4e94bd11f7...0499de31b9)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 08:15:00 +02:00
renovate[bot]
0efa4f81d4 Update octokit monorepo to v8.0.3 (#27757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 08:14:39 +02:00
Aarni Koskela
3ad2f35f29 Add support for PM4 sensor state (#27754) 2025-11-02 16:54:40 +00:00
renovate[bot]
7a21d5f7bc Update dependency @rspack/core to v1.6.0 (#27753)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-02 17:48:11 +01:00
Wesley Vos
33226587e6 Fix translation keys of energy-compare card (#27747) 2025-11-01 19:03:42 +02:00
Jan-Philipp Benecke
bd2673f311 Use progress ring for updates on config dashboard (#27731)
* Use progress ring for updates on config dashboard

* Prcoess code review
2025-11-01 18:57:25 +02:00
renovate[bot]
cecadde497 Update vitest monorepo to v4.0.5 (#27748)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-01 18:56:23 +02:00
Jan-Philipp Benecke
494b8811d0 Fix button text overflow (#27744) 2025-11-01 10:38:33 +01:00
renovate[bot]
4e0a49b3da Update vaadinWebComponents monorepo to v24.9.4 (#27738)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-31 19:50:31 +02:00
renovate[bot]
3145fed5dc Update dependency @material/web to v2.4.1 (#27729)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-31 06:57:37 +01:00
renovate[bot]
3dd040fdc7 Update dependency tar to v7.5.2 [SECURITY] (#27728)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-31 06:57:08 +01:00
Paul Bottein
e3abe9736c Don't show tooltip for ha button menu in top bar (#27723) 2025-10-30 18:17:38 +01:00
Paul Bottein
fe41e72774 Revert "Fix entities card size and add grid contstraints" (#27725) 2025-10-30 18:16:49 +01:00
Paul Bottein
7078ef52d4 Use entity naming in more cards (#27714)
* Use entity naming in more cards

* Migrate statistic card

* Fix localize
2025-10-30 16:58:52 +01:00
Paul Bottein
f1c9802ee3 Revert entity naming in target picker chips (#27722) 2025-10-30 16:24:09 +01:00
Aidan Timson
35697e3f94 Calendar card height: account for title and stop overflow (#27707) 2025-10-30 14:47:27 +00:00
58 changed files with 1157 additions and 834 deletions

View File

@@ -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

View File

@@ -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`

View File

@@ -39,6 +39,7 @@ const SENSOR_DEVICE_CLASSES = [
"pm1",
"pm10",
"pm25",
"pm4",
"power_factor",
"power",
"precipitation",

View File

@@ -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.6",
"@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.4",
"@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.4",
"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"

View File

@@ -214,6 +214,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
"pm1",
"pm10",
"pm25",
"pm4",
"power_factor",
"power",
"pressure",

View File

@@ -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}

View File

@@ -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 {

View File

@@ -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}

View File

@@ -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);
}
`,
];

View File

@@ -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,
};
}

View File

@@ -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 = [

View File

@@ -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",
},

View File

@@ -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) {

View File

@@ -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

View File

@@ -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;
}
`,
];

View File

@@ -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, {

View File

@@ -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>
`;
}

View File

@@ -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>
`;

View File

@@ -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;

View File

@@ -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`

View File

@@ -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>`
: "";
@@ -183,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">
@@ -209,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}
@@ -233,6 +248,7 @@ export class AssistPipelineDebug extends LitElement {
</div>`
: ""}
${dataMinusKeysRender(
this.hass,
this.pipelineRun.wake_word,
WAKE_WORD_DATA
)}
@@ -249,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,
@@ -260,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>
`
: ""}
@@ -285,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}
@@ -303,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}
@@ -312,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
)}
@@ -340,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>
`
: ""}
@@ -361,7 +426,13 @@ export class AssistPipelineDebug extends LitElement {
? this._stopTTS
: this._playTTS}
>
${this._isPlaying ? "Stop audio" : "Play audio"}
${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>
`
@@ -372,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
@@ -391,7 +466,14 @@ export class AssistPipelineDebug extends LitElement {
this._audioElement = new Audio(url);
this._audioElement.addEventListener("error", () => {
showAlertDialog(this, { title: "Error", text: "Error playing audio" });
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"
),
});
});
this._audioElement.addEventListener("play", () => {

View File

@@ -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>
`;
}

View File

@@ -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
);
}
`;
}

View File

@@ -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(

View File

@@ -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

View File

@@ -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() {

View File

@@ -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";

View File

@@ -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`

View File

@@ -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];
}

View File

@@ -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"
@@ -348,7 +348,15 @@ export interface LogbookCardConfig extends LovelaceCardConfig {
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;
@@ -363,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;
@@ -435,7 +443,7 @@ export interface StatisticsGraphCardConfig extends EnergyCardBaseConfig {
}
export interface StatisticCardConfig extends LovelaceCardConfig {
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
entities: (EntityConfig | string)[];
period:
| {

View File

@@ -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:

View File

@@ -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

View File

@@ -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,

View File

@@ -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">

View File

@@ -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> = {};

View File

@@ -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() {

View File

@@ -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",

View File

@@ -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: {

View File

@@ -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")

View File

@@ -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 };

View File

@@ -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: {

View File

@@ -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()),

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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}

View File

@@ -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

View File

@@ -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=${

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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">

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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",
@@ -7088,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": {
@@ -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",

View File

@@ -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", () => {

714
yarn.lock

File diff suppressed because it is too large Load Diff