Compare commits

..

118 Commits

Author SHA1 Message Date
Bram Kragten
0408734ec5 Bumped version to 20251029.1 2025-10-30 18:19:31 +01:00
Paul Bottein
317519fc08 Revert "Fix entities card size and add grid contstraints" (#27725) 2025-10-30 18:18:27 +01:00
Paul Bottein
843d79eab4 Don't show tooltip for ha button menu in top bar (#27723) 2025-10-30 18:18:26 +01:00
Paul Bottein
165a757f06 Revert entity naming in target picker chips (#27722) 2025-10-30 18:18:25 +01:00
Aidan Timson
ea8b730142 Revert "Migrate dialog-device-registry-detail to ha-wa-dialog (#27668)" (#27716)
This reverts commit 2a8d935601.
2025-10-30 18:18:24 +01:00
Paul Bottein
e88c97d625 Use entity naming in more cards (#27714)
* Use entity naming in more cards

* Migrate statistic card

* Fix localize
2025-10-30 18:18:23 +01:00
Aidan Timson
7560988b76 Trend feature: make sure content is centered when loading (#27708)
* Make sure content is centered when loading

* Restore from test
2025-10-30 18:18:22 +01:00
Aidan Timson
eecd8077b6 Calendar card height: account for title and stop overflow (#27707) 2025-10-30 18:18:21 +01:00
Paul Bottein
cbab5c3f7b Restore trigger id in overflow menu for trigger (#27702) 2025-10-30 18:18:20 +01:00
Paul Bottein
a5d27c8bb8 Only clear from and to trigger in state trigger (#27700) 2025-10-30 18:18:19 +01:00
Paul Bottein
a6a340b5db Only display add button if at least one entity is selected in entities picker (#27699) 2025-10-30 18:18:18 +01:00
Bram Kragten
8f5875c30f Merge branch 'rc' into dev 2025-10-29 16:20:58 +01:00
Bram Kragten
517cd49f35 Bumped version to 20251029.0 2025-10-29 16:19:23 +01:00
Petar Petrov
25d9fc94b2 Add legend to energy pie chart (#27697)
* Add legend to energy pie chart

* Update src/components/chart/ha-chart-base.ts

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

* resize fix

* some fixes

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2025-10-29 14:50:57 +00:00
Ezra Freedman
7b188759e3 Fix todo item date picker displaying previous day (#27698) 2025-10-29 14:21:34 +00:00
Paul Bottein
76772d1098 Default card name to friendly name (#27696)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-10-29 13:41:45 +00:00
Paul Bottein
6052745ca0 Add floor icon to every home dashboard views (#27695) 2025-10-29 13:36:28 +01:00
Paul Bottein
89b9780345 Revert "Default entity name to friendly name"
This reverts commit a607edca96.
2025-10-29 13:02:55 +01:00
Paul Bottein
a607edca96 Default entity name to friendly name 2025-10-29 13:02:14 +01:00
Tobias Bieniek
52eb3d8063 Add automations category to home dashboard area views (#27641) 2025-10-29 12:21:17 +01:00
renovate[bot]
1361fc36bf Update dependency @lezer/highlight to v1.2.3 (#27691)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-29 10:44:54 +00:00
Tobias Bieniek
505ef2bd11 home dashboard: Allow users to choose weather entity if they have more than one (#27643) 2025-10-29 11:36:05 +01:00
Petar Petrov
c0cc66c1ab Fix next flow config flow showing an empty dialog (#27682) 2025-10-29 09:53:14 +01:00
ildar170975
7cfbc521c7 Dev tools -> Templates: max-height fix for cm-editor (#27461) 2025-10-29 09:52:45 +01:00
Ezra Freedman
e064ce56cc Fix calendar all day date display (#27689) 2025-10-29 09:42:50 +01:00
renovate[bot]
8d688aa3a9 Update Node.js to v22.21.1 (#27686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 19:46:07 +00:00
Aidan Timson
d122483449 Fix entities card size and add grid contstraints (#27684)
* Add grid card options

* Allow overflow

* Use ha-scrollbar

* Use title/header for min rows calculation

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Format

* Remove entities length check

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-10-28 20:35:55 +01:00
Aidan Timson
f17bbc3f79 Fix activity card height and add constraints for grid layout (#27683)
* Fix logbook height

* Add grid option constraints

* Reverse
2025-10-28 17:02:50 +02:00
karwosts
c88f8fcce0 Shift stats in history by 1 hour (#27633) 2025-10-28 15:53:22 +02:00
Tobias Bieniek
8efabde916 Add floor icons to home dashboard headings (#27639)
* Add floor icons to home dashboard headings

This displays floor icons next to floor names in the home dashboard to provide visual consistency with the areas overview dashboard. The icons use either the custom floor icon if configured, or fall back to level-based default icons (e.g., `mdi:home-floor-0`, `mdi:home-floor-1`).

* Remove floor icon fallback from home dashboard headings

as requested in https://github.com/home-assistant/frontend/pull/27639#issuecomment-3452048655
2025-10-28 15:50:50 +02:00
Niklas Wagner
e821e1ec83 Allow selecting multiple states in trigger condition (#27455)
* Allow selecting multiple states in trigger condition

* Make from/to select exlusive to each other

* Simplify code

* fix: returning correct type

* Remove unnecessary any type
2025-10-28 15:43:34 +02:00
Wendelin
dc7516da94 Bottom-sheet swipe to close (#27537)
* WIP new add automation element

* WIP new add dialog

* revert merge

* Add tabs

* fix height

* Add max-height

* Add keybindings and blocks search separation

* Fix device translation

* add swipe to close for bottom sheet

* fix translations, scroll issues, RTL

* update target picker selector

* Fix bottom sheet padding

* Simplify scroll lock

* Simplify scroll lock

* Improve swipe gesture

* Fix methods

* Fix race condition

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2025-10-28 13:47:11 +02:00
Aidan Timson
a545a377a7 Fix typos and improve grammar on ha-dialogs design docs (#27681) 2025-10-28 12:38:47 +01:00
Aidan Timson
3634dbcbbf Add media player volume buttons card feature (#27624)
* Add media player volume buttons card feature

* Sort import

* Add uom

* Update src/panels/lovelace/card-features/hui-media-player-volume-buttons-card-feature.ts

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-10-28 13:29:56 +02:00
renovate[bot]
75af4f939e Update vitest monorepo to v4.0.3 (#27673)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 14:42:03 +00:00
Wendelin
453a2ac7f3 Use generic picker for language picker (#27631)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-10-27 14:02:13 +00:00
karwosts
8fbd0226fc Media selector for view backgrounds (#27544)
* Media selector for view backgrounds

* Bring back preview image
2025-10-27 15:26:20 +02:00
Aidan Timson
2a8d935601 Migrate dialog-device-registry-detail to ha-wa-dialog (#27668) 2025-10-27 14:30:22 +02:00
Aidan Timson
a6328fb6d7 Use space tokens in ha-more-info-dialog (#27666) 2025-10-27 13:44:49 +02:00
Aidan Timson
a78b61006f Use space tokens in ha-automation-row (#27665) 2025-10-27 13:40:37 +02:00
Aidan Timson
d506aa23b6 Use space tokens in ha-markdown (#27664) 2025-10-27 13:40:04 +02:00
Aidan Timson
48b4df43ab Use space tokens in ha-quick-bar (#27663) 2025-10-27 13:39:41 +02:00
Aidan Timson
8cdcd9cb55 Use space tokens in ha-card (#27662) 2025-10-27 13:38:36 +02:00
ildar170975
a1e2ac1d99 Label picker/selector: add a decription to a list (#27635)
add a decription to getLabels()
2025-10-27 11:11:53 +02:00
Simon Lamon
8ecddbc42c Adjust primary action buttons for create/update category/area dialogs (#27651)
Adjust primary action buttons
2025-10-27 10:52:02 +02:00
dependabot[bot]
6f70ef52a5 Bump actions/upload-artifact from 4.6.2 to 5.0.0 (#27660)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 5.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](ea165f8d65...330a01c490)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 10:49:12 +02:00
renovate[bot]
7dff02d7c8 Update dependency @types/sortablejs to v1.15.9 (#27659)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 06:35:59 +00:00
Simon Lamon
8bbd7a6a06 Add min and max values for hardware graphs (#27649) 2025-10-27 08:31:24 +02:00
karwosts
5c73a06f76 Regenerate service-picker valueRenderer on localize update (#27640)
* Regenerate service-picker valueRenderer on localize update

* memoize services
2025-10-27 08:30:17 +02:00
Simon Lamon
9943dae82c Adjust line height in weather card (#27653) 2025-10-27 08:29:17 +02:00
renovate[bot]
70bf049df0 Update babel monorepo to v7.28.5 (#27655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 08:28:29 +02:00
renovate[bot]
f9d9fbb7f0 Update vitest monorepo to v4.0.2 (#27656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 08:28:00 +02:00
dependabot[bot]
9cb84d3f37 Bump github/codeql-action from 4.30.9 to 4.31.0 (#27661)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.30.9 to 4.31.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](16140ae1a1...4e94bd11f7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 08:27:30 +02:00
renovate[bot]
c1bcf27cf8 Update dependency @types/qrcode to v1.5.6 (#27658)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 08:26:41 +02:00
Paul Bottein
3e749ec085 20251001.4 (#27458) 2025-10-11 14:42:36 +02:00
Paul Bottein
ee2ec00069 Bumped version to 20251001.4 2025-10-11 14:41:59 +02:00
Paul Bottein
0aa2941868 Fix unresolved merge conflict in core style 2025-10-11 14:41:42 +02:00
Paul Bottein
46cd1d5156 20251001.3 (#27457) 2025-10-11 14:28:02 +02:00
Paul Bottein
07a5c41fd4 Bumped version to 20251001.3 2025-10-11 14:27:11 +02:00
Paul Bottein
4ad3c553d5 Adjust yarn updates for rc (#27456) 2025-10-11 14:26:10 +02:00
Simon Lamon
d40cc448a5 Adjust yarn updates for rc 2025-10-11 12:08:06 +00:00
Bram Kragten
e2f3f9d348 Merge branch 'rc' 2025-10-10 11:36:56 +02:00
Bram Kragten
98d44950f8 Bumped version to 20251001.2 2025-10-10 11:36:46 +02:00
Paul Bottein
8ae9edb1ef Fix ha dialog default size (#27415)
* Don't hardcode width height on mobile for all dialogs

* Don't set min width on desktop
2025-10-10 11:35:45 +02:00
Paul Bottein
84c4396c13 Add tooltip instead of title for 'add' button (#27399) 2025-10-10 11:35:44 +02:00
Bram Kragten
2b937a30e3 Merge branch 'rc' 2025-10-10 10:38:43 +02:00
Bram Kragten
b7815bfd86 Bumped version to 20251001.1 2025-10-10 10:38:26 +02:00
Wendelin
d94fa03411 Fix ha-button keyboard focus (#27437) 2025-10-10 10:38:08 +02:00
Petar Petrov
0a7007ef9e Escape device names in energy dashboard (#27425) 2025-10-10 10:31:09 +02:00
Paul Bottein
dd12136dee Use right variable for content color in tooltip (#27400) 2025-10-10 10:31:08 +02:00
Wendelin
6e2f89fe3d Fix android tap highlight border radius (#27382) 2025-10-10 10:31:07 +02:00
Jan-Philipp Benecke
092085b9af Fix media player more info title calculations (#27360) 2025-10-10 10:31:06 +02:00
Paulus Schoutsen
1c06eb8661 Add ESPHome to discovery sources (#27327) 2025-10-10 10:31:05 +02:00
Jan-Philipp Benecke
c7e87b06b5 Fix formatting of position slider tooltip in media player more info (#27326) 2025-10-10 10:31:04 +02:00
Jan-Philipp Benecke
38c738c199 Add "media_stop" action to media player controls in more info (#27325) 2025-10-10 10:31:03 +02:00
Wendelin
e899587307 Fix mobile ha-dialog height in Browser (#27298)
Enhance dialog responsiveness by adjusting min/max height to use svh units
2025-10-10 10:31:02 +02:00
Jan-Philipp Benecke
c9feb0b75f Add color tokens for slider thumb and indicator (#27295) 2025-10-10 10:31:00 +02:00
Jan-Philipp Benecke
10718c35d1 Make ha-slider not depend on font sizes (#27294) 2025-10-10 10:30:59 +02:00
Wendelin
4dc6a37bad Fix automation editor safe area (#27292) 2025-10-10 10:30:59 +02:00
Jan-Philipp Benecke
ac49fc7aba Support redo on Shift+CMD+Z (#27287)
* Support redo on Shift+CMD+Z

* Update redo shortcut for macOS to CMD+Shift+Z
2025-10-10 10:30:57 +02:00
Paul Bottein
e4f008800b Align bottom sheet border radius with resizable bottom sheet (#27280) 2025-10-10 10:30:56 +02:00
Bram Kragten
0b0ffd7bab Merge branch 'rc' 2025-10-01 08:58:47 +02:00
Bram Kragten
dfa77526a2 Bumped version to 20251001.0 2025-10-01 08:58:30 +02:00
Bram Kragten
9a3bd6c613 Fix intl polyfill loading (#27261) 2025-10-01 08:55:19 +02:00
Wendelin
1161de5746 Update WA to fix tab group scrolling (#27255) 2025-10-01 08:51:12 +02:00
Jan-Philipp Benecke
9df8e20391 Use local entity picture if available in media player more info (#27252) 2025-10-01 08:51:11 +02:00
Petar Petrov
11047a9c95 Make "loading next step" look like progress step in config flows (#27234) 2025-10-01 08:51:10 +02:00
Jan-Philipp Benecke
18fa66f61c Adjust media player cover image sizes in more info for smaller screens (#27232)
Adjust media player cover image sizes for smaller screens
2025-10-01 08:51:09 +02:00
Simon Lamon
758a048f34 Set explicit netlify version to fix workflows (#27229)
netlify set explicit version for fix
2025-10-01 08:51:08 +02:00
Jan-Philipp Benecke
ee0fc360b0 Add custom color token for control color (#27227) 2025-10-01 08:51:07 +02:00
Jan-Philipp Benecke
4012f95ec1 Add tooltips for undo/redo in automation & script editors (#27224) 2025-10-01 08:51:06 +02:00
Paul Bottein
0336ce4606 20250926.0 (#27213) 2025-09-26 15:39:29 +02:00
Paul Bottein
9ba36ab7e2 Bumped version to 20250926.0 2025-09-26 15:38:51 +02:00
Paul Bottein
fe7a08a1b0 Don't display negative durations in media player more info (#27212)
Don't display negative value in media player more info
2025-09-26 15:38:21 +02:00
Paul Bottein
87a8f9cedc Fix slider ticks support for number selector (#27211) 2025-09-26 15:38:21 +02:00
Paul Bottein
01df7e20ca Fix try tts dialog max width (#27208) 2025-09-26 15:38:20 +02:00
Jan-Philipp Benecke
d181219522 Refactor media player slider to use slot for position and duration display (#27205)
* Refactor media player slider to use slot for position and duration display

* Fix variable naming
2025-09-26 15:38:19 +02:00
karwosts
6ae24b8135 Add validation issues to energy diagnostic (#27203) 2025-09-26 15:38:18 +02:00
karwosts
8e009f24f9 Add dropdown mode to water heater operation feature (#27201) 2025-09-26 15:38:17 +02:00
Simon Lamon
53031f44ac Fix typos in media player more info (#27198) 2025-09-26 15:38:16 +02:00
Jan-Philipp Benecke
af5a988457 Round seconds in media player more info before formatting (#27196) 2025-09-26 15:38:15 +02:00
Paul Bottein
bab0391a19 20250925.1 (#27191) 2025-09-25 17:57:22 +02:00
Paul Bottein
444123c47e Bumped version to 20250925.1 2025-09-25 17:56:13 +02:00
Paul Bottein
f123d34046 Revert "Update dependency @types/chromecast-caf-receiver to v6.0.24" (#27188) 2025-09-25 17:53:59 +02:00
Paul Bottein
1b40f99f68 Fix storage bar not displayed (#27183) 2025-09-25 17:50:52 +02:00
Paul Bottein
b314b3ed2b Fix analytics switches (#27181) 2025-09-25 17:50:51 +02:00
Paul Bottein
59b8932969 Add icon option to common controls section strategy (#27180) 2025-09-25 17:50:50 +02:00
Wendelin
107af753ec Reduce default tab padding in tab-group (#27173) 2025-09-25 17:50:49 +02:00
Paul Bottein
1f0acb3046 Disabled config badge (#27172)
* Add disabled option for badge

* Add disabled to struct
2025-09-25 17:50:48 +02:00
Paul Bottein
431e533929 20250925.0 (#27170) 2025-09-25 10:48:30 +02:00
Paul Bottein
02c845cbc6 Bumped version to 20250925.0 2025-09-25 10:47:41 +02:00
Paul Bottein
628111ed20 Bumped version to 20250924.1 2025-09-25 10:46:44 +02:00
Paul Bottein
e825a9c02f Smooth animation of the sidebar resizing handle (#27166) 2025-09-25 10:46:36 +02:00
Paul Bottein
7a35bddf36 Fix safe padding for bottom sheet and add scroll lock (#27165) 2025-09-25 10:46:35 +02:00
Norbert Rittel
ad69270af8 Use "Add (person)" instead of "New person" / "Create" (#27161)
* Update dialog-person-detail.ts

* Update en.json
2025-09-25 10:46:34 +02:00
Paulus Schoutsen
404edf9483 Avoid invalid entities in common controls (#27158) 2025-09-25 10:46:33 +02:00
Paul Bottein
a166b4e9b6 Do not show error message when action has no response in dev tools (#27156) 2025-09-25 10:46:32 +02:00
Paul Bottein
7a285f11db 20250924.0 (#27155) 2025-09-24 17:15:37 +02:00
119 changed files with 1887 additions and 1458 deletions

View File

@@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: frontend-bundle-stats
path: build/stats/*.json
@@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: supervisor-bundle-stats
path: build/stats/*.json

View File

@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
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@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
# 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@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0

View File

@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: translations
path: translations.tar.gz

2
.nvmrc
View File

@@ -1 +1 @@
22.21.0
22.21.1

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

@@ -5,14 +5,14 @@ subtitle: Dialogs provide important prompts in a user flow.
# Material Design 3
Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guideliness. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guidelines. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
# Guidelines
## Design
- Dialogs have a max width of 560px. Alert and confirmation dialogs got a fixed width of 320px. If you need more width, consider a dedicated page instead.
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guideliness.
- Dialogs have a max width of 560px. Alert and confirmation dialogs have a fixed width of 320px. If you need more width, consider a dedicated page instead.
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guidelines.
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake.
- Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu.
- The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom.
@@ -26,7 +26,7 @@ Our dialogs are based on the latest version of Material Design. Please note that
- A best practice is to always use a title, even if it is optional by Material guidelines.
- People mainly read the title and a button. Put the most important information in those two.
- Try to avoid user generated content in the title, this could make the title unreadable long.
- Try to avoid user generated content in the title, this could make the title unreadably long.
- If users become unsure, they read the description. Make sure this explains what will happen.
- Strive for minimalism.

View File

@@ -53,7 +53,7 @@
"@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@home-assistant/webawesome": "3.0.0-beta.6.ha.6",
"@lezer/highlight": "1.2.2",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6",
"@lit-labs/virtualizer": "2.1.1",
@@ -148,10 +148,10 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.28.4",
"@babel/core": "7.28.5",
"@babel/helper-define-polyfill-provider": "0.6.5",
"@babel/plugin-transform-runtime": "7.28.3",
"@babel/preset-env": "7.28.3",
"@babel/plugin-transform-runtime": "7.28.5",
"@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",
@@ -173,12 +173,12 @@
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.7.1",
"@types/mocha": "10.0.10",
"@types/qrcode": "1.5.5",
"@types/sortablejs": "1.15.8",
"@types/qrcode": "1.5.6",
"@types/sortablejs": "1.15.9",
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.1",
"@vitest/coverage-v8": "4.0.3",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
@@ -219,7 +219,7 @@
"typescript": "5.9.3",
"typescript-eslint": "8.46.2",
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.1",
"vitest": "4.0.3",
"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"
@@ -235,5 +235,8 @@
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
},
"packageManager": "yarn@4.10.3"
"packageManager": "yarn@4.10.3",
"volta": {
"node": "22.21.1"
}
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20250924.0"
version = "20251029.1"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"

View File

@@ -0,0 +1,116 @@
export interface SwipeGestureResult {
velocity: number;
delta: number;
isSwipe: boolean;
isDownwardSwipe: boolean;
}
export interface SwipeGestureConfig {
velocitySwipeThreshold?: number;
movementTimeThreshold?: number;
}
const VELOCITY_SWIPE_THRESHOLD = 0.5; // px/ms
const MOVEMENT_TIME_THRESHOLD = 100; // ms
/**
* Recognizes swipe gestures and calculates velocity for touch interactions.
* Tracks touch movement and provides velocity-based and position-based gesture detection.
*/
export class SwipeGestureRecognizer {
private _startY = 0;
private _delta = 0;
private _startTime = 0;
private _lastY = 0;
private _lastTime = 0;
private _velocityThreshold: number;
private _movementTimeThreshold: number;
constructor(config: SwipeGestureConfig = {}) {
this._velocityThreshold =
config.velocitySwipeThreshold ?? VELOCITY_SWIPE_THRESHOLD; // px/ms
this._movementTimeThreshold =
config.movementTimeThreshold ?? MOVEMENT_TIME_THRESHOLD; // ms
}
/**
* Initialize gesture tracking with starting touch position
*/
public start(clientY: number): void {
const now = Date.now();
this._startY = clientY;
this._startTime = now;
this._lastY = clientY;
this._lastTime = now;
this._delta = 0;
}
/**
* Update gesture state during movement
* Returns the current delta (negative when dragging down)
*/
public move(clientY: number): number {
const now = Date.now();
this._delta = this._startY - clientY;
this._lastY = clientY;
this._lastTime = now;
return this._delta;
}
/**
* Calculate final gesture result when touch ends
*/
public end(): SwipeGestureResult {
const velocity = this.getVelocity();
const hasSignificantVelocity = Math.abs(velocity) > this._velocityThreshold;
return {
velocity,
delta: this._delta,
isSwipe: hasSignificantVelocity,
isDownwardSwipe: velocity > 0,
};
}
/**
* Get current drag delta (negative when dragging down)
*/
public getDelta(): number {
return this._delta;
}
/**
* Calculate velocity based on recent movement
* Returns 0 if no recent movement detected
* Positive velocity means downward swipe
*/
public getVelocity(): number {
const now = Date.now();
const timeSinceLastMove = now - this._lastTime;
// Only consider velocity if the last movement was recent
if (timeSinceLastMove >= this._movementTimeThreshold) {
return 0;
}
const timeDelta = this._lastTime - this._startTime;
return timeDelta > 0 ? (this._lastY - this._startY) / timeDelta : 0;
}
/**
* Reset all tracking state
*/
public reset(): void {
this._startY = 0;
this._delta = 0;
this._startTime = 0;
this._lastY = 0;
this._lastTime = 0;
}
}

View File

@@ -35,6 +35,7 @@ export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
const LEGEND_OVERFLOW_LIMIT = 10;
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
const DOUBLE_TAP_TIME = 300;
const RESIZE_ANIMATION_DURATION = 250;
export type CustomLegendOption = ECOption["legend"] & {
type: "custom";
@@ -205,6 +206,15 @@ export class HaChartBase extends LitElement {
}
if (changedProps.has("options")) {
chartOptions = { ...chartOptions, ...this._createOptions() };
if (
this._compareCustomLegendOptions(
changedProps.get("options"),
this.options
)
) {
// custom legend changes may require a resize to layout properly
this._shouldResizeChart = true;
}
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
chartOptions.dataZoom = this._getDataZoomConfig();
}
@@ -296,7 +306,7 @@ export class HaChartBase extends LitElement {
itemStyle = {
color: dataset?.color as string,
...(dataset?.itemStyle as { borderColor?: string }),
itemStyle,
...itemStyle,
};
const color = itemStyle?.color as string;
const borderColor = itemStyle?.borderColor as string;
@@ -508,6 +518,7 @@ export class HaChartBase extends LitElement {
);
}
});
this.requestUpdate("_hiddenDatasets");
}
private _getDataZoomConfig(): DataZoomComponentOption | undefined {
@@ -958,11 +969,31 @@ export class HaChartBase extends LitElement {
private _handleChartRenderFinished = () => {
if (this._shouldResizeChart) {
this.chart?.resize();
this.chart?.resize({
animation: this._reducedMotion
? undefined
: { duration: RESIZE_ANIMATION_DURATION },
});
this._shouldResizeChart = false;
}
};
private _compareCustomLegendOptions(
oldOptions: ECOption | undefined,
newOptions: ECOption | undefined
): boolean {
const oldLegends = ensureArray(
oldOptions?.legend || []
) as LegendComponentOption[];
const newLegends = ensureArray(
newOptions?.legend || []
) as LegendComponentOption[];
return (
oldLegends.some((l) => l.show && l.type === "custom") !==
newLegends.some((l) => l.show && l.type === "custom")
);
}
static styles = css`
:host {
display: block;

View File

@@ -0,0 +1 @@
export const ANY_STATE_VALUE = "__ANY_STATE_IGNORE_ATTRIBUTES__";

View File

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

View File

@@ -312,7 +312,7 @@ export class HaEntityNamePicker extends LitElement {
private _toValue = memoizeOne(
(items: EntityNameItem[]): typeof this.value => {
if (items.length === 0) {
return "";
return undefined;
}
if (items.length === 1) {
const item = items[0];

View File

@@ -4,6 +4,7 @@ import { customElement, property } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../common/dom/fire_event";
import { ANY_STATE_VALUE } from "./const";
import { ensureArray } from "../../common/array/ensure-array";
import type { HomeAssistant } from "../../types";
import "./ha-entity-state-picker";
@@ -57,6 +58,7 @@ export class HaEntityStatesPicker extends LitElement {
const value = this.value || [];
const hide = [...(this.hideStates || []), ...value];
const hideValue = value.includes(ANY_STATE_VALUE);
return html`
${repeat(
@@ -84,7 +86,7 @@ export class HaEntityStatesPicker extends LitElement {
`
)}
<div>
${this.disabled && value.length
${(this.disabled && value.length) || hideValue
? nothing
: keyed(
value.length,

View File

@@ -118,7 +118,7 @@ export class HaAutomationRow extends LitElement {
}
.row {
display: flex;
padding: 0 8px;
padding: var(--ha-space-0) var(--ha-space-2);
min-height: 48px;
align-items: center;
cursor: pointer;
@@ -134,12 +134,12 @@ export class HaAutomationRow extends LitElement {
.expand-button {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
color: var(--ha-color-on-neutral-quiet);
margin-left: -8px;
margin-left: calc(var(--ha-space-2) * -1);
}
:host([building-block]) .leading-icon-wrapper {
background-color: var(--ha-color-fill-neutral-loud-resting);
border-radius: var(--ha-border-radius-md);
padding: 4px;
padding: var(--ha-space-1);
display: flex;
justify-content: center;
align-items: center;
@@ -149,7 +149,7 @@ export class HaAutomationRow extends LitElement {
color: var(--ha-color-on-neutral-quiet);
}
:host([building-block]) ::slotted([slot="leading-icon"]) {
--mdc-icon-size: 20px;
--mdc-icon-size: var(--ha-space-5);
color: var(--white-color);
transform: rotate(-45deg);
}
@@ -170,7 +170,7 @@ export class HaAutomationRow extends LitElement {
::slotted([slot="header"]) {
flex: 1;
overflow-wrap: anywhere;
margin: 0 12px;
margin: var(--ha-space-0) var(--ha-space-3);
}
:host([sort-selected]) .row {
outline: solid;

View File

@@ -1,6 +1,7 @@
import "@home-assistant/webawesome/dist/components/drawer/drawer";
import { css, html, LitElement, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
import { haStyleScrollbar } from "../resources/styles";
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
@@ -14,6 +15,12 @@ export class HaBottomSheet extends LitElement {
@state() private _drawerOpen = false;
@query("#drawer") private _drawer!: HTMLElement;
private _gestureRecognizer = new SwipeGestureRecognizer();
private _isDragging = false;
private _handleAfterHide() {
this.open = false;
const ev = new Event("closed", {
@@ -33,19 +40,132 @@ export class HaBottomSheet extends LitElement {
render() {
return html`
<wa-drawer
id="drawer"
placement="bottom"
.open=${this._drawerOpen}
@wa-after-hide=${this._handleAfterHide}
without-header
@touchstart=${this._handleTouchStart}
>
<slot name="header"></slot>
<div class="body ha-scrollbar">
<div id="body" class="body ha-scrollbar">
<slot></slot>
</div>
</wa-drawer>
`;
}
private _handleTouchStart = (ev: TouchEvent) => {
// Check if any element inside drawer in the composed path has scrollTop > 0
for (const path of ev.composedPath()) {
const el = path as HTMLElement;
if (el === this._drawer) {
break;
}
if (el.scrollTop > 0) {
return;
}
}
this._startResizing(ev.touches[0].clientY);
};
private _startResizing(clientY: number) {
// register event listeners for drag handling
document.addEventListener("touchmove", this._handleTouchMove, {
passive: false,
});
document.addEventListener("touchend", this._handleTouchEnd);
document.addEventListener("touchcancel", this._handleTouchEnd);
this._gestureRecognizer.start(clientY);
}
private _handleTouchMove = (ev: TouchEvent) => {
const currentY = ev.touches[0].clientY;
const delta = this._gestureRecognizer.move(currentY);
if (delta < 0) {
ev.preventDefault();
this._isDragging = true;
requestAnimationFrame(() => {
if (this._isDragging) {
this.style.setProperty(
"--dialog-transform",
`translateY(${delta * -1}px)`
);
}
});
}
};
private _animateSnapBack() {
// Add transition for smooth animation
this.style.setProperty(
"--dialog-transition",
`transform ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms ease-out`
);
// Reset transform to snap back
this.style.removeProperty("--dialog-transform");
// Remove transition after animation completes
setTimeout(() => {
this.style.removeProperty("--dialog-transition");
}, BOTTOM_SHEET_ANIMATION_DURATION_MS);
}
private _handleTouchEnd = () => {
this._unregisterResizeHandlers();
this._isDragging = false;
const result = this._gestureRecognizer.end();
// If velocity exceeds threshold, use velocity direction to determine action
if (result.isSwipe) {
if (result.isDownwardSwipe) {
// Downward swipe - close the bottom sheet
this._drawerOpen = false;
} else {
// Upward swipe - keep open and animate back
this._animateSnapBack();
}
return;
}
// If velocity is below threshold, use position-based logic
// Get the drawer height to calculate 50% threshold
const drawerBody = this._drawer.shadowRoot?.querySelector(
'[part="body"]'
) as HTMLElement;
const drawerHeight = drawerBody?.offsetHeight || 0;
// delta is negative when dragging down
// Close if dragged down past 50% of the drawer height
if (
drawerHeight > 0 &&
result.delta < 0 &&
Math.abs(result.delta) > drawerHeight * 0.5
) {
this._drawerOpen = false;
} else {
this._animateSnapBack();
}
};
private _unregisterResizeHandlers = () => {
document.removeEventListener("touchmove", this._handleTouchMove);
document.removeEventListener("touchend", this._handleTouchEnd);
document.removeEventListener("touchcancel", this._handleTouchEnd);
};
disconnectedCallback() {
super.disconnectedCallback();
this._unregisterResizeHandlers();
this._isDragging = false;
}
static styles = [
haStyleScrollbar,
css`
@@ -59,6 +179,8 @@ export class HaBottomSheet extends LitElement {
wa-drawer::part(dialog) {
max-height: var(--ha-bottom-sheet-max-height, 90vh);
align-items: center;
transform: var(--dialog-transform);
transition: var(--dialog-transition);
}
wa-drawer::part(body) {
max-width: var(--ha-bottom-sheet-max-width);
@@ -90,6 +212,11 @@ export class HaBottomSheet extends LitElement {
max-width: 100%;
display: flex;
flex-direction: column;
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
}
`,
];

View File

@@ -44,26 +44,26 @@ export class HaCard extends LitElement {
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
letter-spacing: -0.012em;
line-height: var(--ha-line-height-expanded);
padding: 12px 16px 16px;
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4);
display: block;
margin-block-start: 0px;
margin-block-end: 0px;
margin-block-start: var(--ha-space-0);
margin-block-end: var(--ha-space-0);
font-weight: var(--ha-font-weight-normal);
}
:host ::slotted(.card-content:not(:first-child)),
slot:not(:first-child)::slotted(.card-content) {
padding-top: 0px;
margin-top: -8px;
padding-top: var(--ha-space-0);
margin-top: calc(var(--ha-space-2) * -1);
}
:host ::slotted(.card-content) {
padding: 16px;
padding: var(--ha-space-4);
}
:host ::slotted(.card-actions) {
border-top: 1px solid var(--divider-color, #e8e8e8);
padding: 8px;
padding: var(--ha-space-2);
}
`;

View File

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

View File

@@ -24,7 +24,7 @@ import "./ha-svg-icon";
@customElement("ha-generic-picker")
export class HaGenericPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hass?: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@@ -68,6 +68,21 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: "not-found-label", type: String })
public notFoundLabel?: string;
@property({ attribute: "popover-placement" })
public popoverPlacement:
| "bottom"
| "top"
| "left"
| "right"
| "top-start"
| "top-end"
| "right-start"
| "right-end"
| "bottom-start"
| "bottom-end"
| "left-start"
| "left-end" = "bottom-start";
/** If set picker shows an add button instead of textbox when value isn't set */
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
@@ -135,7 +150,7 @@ export class HaGenericPicker extends LitElement {
style="--body-width: ${this._popoverWidth}px;"
without-arrow
distance="-4"
placement="bottom-start"
.placement=${this.popoverPlacement}
for="picker"
auto-size="vertical"
auto-size-padding="16"
@@ -144,9 +159,7 @@ export class HaGenericPicker extends LitElement {
trap-focus
role="dialog"
aria-modal="true"
aria-label=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
aria-label=${this.label || "Select option"}
>
${this._renderComboBox()}
</wa-popover>
@@ -159,9 +172,7 @@ export class HaGenericPicker extends LitElement {
@closed=${this._hidePicker}
role="dialog"
aria-modal="true"
aria-label=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
aria-label=${this.label || "Select option"}
>
${this._renderComboBox(true)}
</ha-bottom-sheet>`
@@ -179,7 +190,8 @@ export class HaGenericPicker extends LitElement {
<ha-picker-combo-box
.hass=${this.hass}
.allowCustomValue=${this.allowCustomValue}
.label=${this.searchLabel ?? this.hass.localize("ui.common.search")}
.label=${this.searchLabel ??
(this.hass?.localize("ui.common.search") || "Search")}
.value=${this.value}
@value-changed=${this._valueChanged}
.rowRenderer=${this.rowRenderer}

View File

@@ -1,56 +1,58 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { formatLanguageCode } from "../common/language/format_language";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { FrontendLocaleData } from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata";
import type { HomeAssistant } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-generic-picker";
import "./ha-list-item";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
import "./ha-select";
import type { HaSelect } from "./ha-select";
export const getLanguageOptions = (
languages: string[],
nativeName: boolean,
noSort: boolean,
locale?: FrontendLocaleData
) => {
let options: { label: string; value: string }[] = [];
): PickerComboBoxItem[] => {
let options: PickerComboBoxItem[] = [];
if (nativeName) {
const translations = translationMetadata.translations;
options = languages.map((lang) => {
let label = translations[lang]?.nativeName;
if (!label) {
let primary = translations[lang]?.nativeName;
if (!primary) {
try {
// this will not work if Intl.DisplayNames is polyfilled, it will return in the language of the user
label = new Intl.DisplayNames(lang, {
primary = new Intl.DisplayNames(lang, {
type: "language",
fallback: "code",
}).of(lang)!;
} catch (_err) {
label = lang;
primary = lang;
}
}
return {
value: lang,
label,
id: lang,
primary,
search_labels: [primary],
};
});
} else if (locale) {
options = languages.map((lang) => ({
value: lang,
label: formatLanguageCode(lang, locale),
id: lang,
primary: formatLanguageCode(lang, locale),
search_labels: [formatLanguageCode(lang, locale)],
}));
}
if (!noSort && locale) {
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, locale.language)
caseInsensitiveStringCompare(a.primary, b.primary, locale.language)
);
}
return options;
@@ -80,115 +82,69 @@ export class HaLanguagePicker extends LitElement {
@state() _defaultLanguages: string[] = [];
@query("ha-select") private _select!: HaSelect;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._computeDefaultLanguageOptions();
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
const localeChanged =
changedProperties.has("hass") &&
this.hass &&
changedProperties.get("hass") &&
changedProperties.get("hass").locale.language !==
this.hass.locale.language;
if (
changedProperties.has("languages") ||
changedProperties.has("value") ||
localeChanged
) {
this._select.layoutOptions();
if (!this.disabled && this._select.value !== this.value) {
fireEvent(this, "value-changed", { value: this._select.value });
}
if (!this.value) {
return;
}
const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages,
this.nativeName,
this.noSort,
this.hass?.locale
);
const selectedItemIndex = languageOptions.findIndex(
(option) => option.value === this.value
);
if (selectedItemIndex === -1) {
this.value = undefined;
}
if (localeChanged) {
this._select.select(selectedItemIndex);
}
}
}
private _getLanguagesOptions = memoizeOne(getLanguageOptions);
private _computeDefaultLanguageOptions() {
this._defaultLanguages = Object.keys(translationMetadata.translations);
}
protected render() {
const languageOptions = this._getLanguagesOptions(
private _getItems = () =>
this._getLanguagesOptions(
this.languages ?? this._defaultLanguages,
this.nativeName,
this.noSort,
this.hass?.locale
);
private _valueRenderer = (value) => {
const language = this._getItems().find(
(lang) => lang.id === value
)?.primary;
return html`<span slot="headline">${language ?? value}</span> `;
};
protected render() {
const value =
this.value ??
(this.required && !this.disabled
? languageOptions[0]?.value
: this.value);
(this.required && !this.disabled ? this._getItems()[0].id : this.value);
return html`
<ha-select
.label=${this.label ??
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
popover-placement="bottom-end"
.notFoundLabel=${this.hass?.localize(
"ui.components.language-picker.no_match"
)}
.placeholder=${this.label ??
(this.hass?.localize("ui.components.language-picker.language") ||
"Language")}
.value=${value || ""}
.required=${this.required}
.value=${value}
.valueRenderer=${this._valueRenderer}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.inlineArrow=${this.inlineArrow}
>
${languageOptions.length === 0
? html`<ha-list-item value=""
>${this.hass?.localize(
"ui.components.language-picker.no_languages"
) || "No languages"}</ha-list-item
>`
: languageOptions.map(
(option) => html`
<ha-list-item .value=${option.value}
>${option.label}</ha-list-item
>
`
)}
</ha-select>
.getItems=${this._getItems}
@value-changed=${this._changed}
hide-clear-icon
></ha-generic-picker>
`;
}
static styles = css`
ha-select {
ha-generic-picker {
width: 100%;
min-width: 200px;
display: block;
}
`;
private _changed(ev): void {
const target = ev.target as HaSelect;
if (this.disabled || target.value === "" || target.value === this.value) {
return;
}
this.value = target.value;
private _changed(ev: ValueChangedEvent<string>): void {
ev.stopPropagation();
this.value = ev.detail.value;
fireEvent(this, "value-changed", { value: this.value });
}
}

View File

@@ -50,7 +50,7 @@ export class HaMarkdown extends LitElement {
}
ha-alert {
display: block;
margin: 4px 0;
margin: var(--ha-space-1) 0;
}
a {
color: var(--primary-color);
@@ -75,7 +75,7 @@ export class HaMarkdown extends LitElement {
padding: 0;
}
pre {
padding: 16px;
padding: var(--ha-space-4);
overflow: auto;
line-height: var(--ha-line-height-condensed);
font-family: var(--ha-font-family-code);
@@ -95,7 +95,7 @@ export class HaMarkdown extends LitElement {
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
margin: var(--ha-space-4) 0;
}
` as CSSResultGroup;
}

View File

@@ -69,7 +69,7 @@ export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
@customElement("ha-picker-combo-box")
export class HaPickerComboBox extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hass?: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@@ -140,7 +140,9 @@ export class HaPickerComboBox extends LitElement {
protected render() {
return html`<ha-textfield
.label=${this.label ?? this.hass.localize("ui.common.search")}
.label=${this.label ??
this.hass?.localize("ui.common.search") ??
"Search"}
@input=${this._filterChanged}
></ha-textfield>
<lit-virtualizer
@@ -159,12 +161,18 @@ export class HaPickerComboBox extends LitElement {
private _defaultNotFoundItem = memoizeOne(
(
label: this["notFoundLabel"],
localize: LocalizeFunc
localize?: LocalizeFunc
): PickerComboBoxItemWithLabel => ({
id: NO_MATCHING_ITEMS_FOUND_ID,
primary: label || localize("ui.components.combo-box.no_match"),
primary:
label ||
(localize && localize("ui.components.combo-box.no_match")) ||
"No matching items found",
icon_path: mdiMagnify,
a11y_label: label || localize("ui.components.combo-box.no_match"),
a11y_label:
label ||
(localize && localize("ui.components.combo-box.no_match")) ||
"No matching items found",
})
);
@@ -189,13 +197,13 @@ export class HaPickerComboBox extends LitElement {
caseInsensitiveStringCompare(
entityA.sorting_label!,
entityB.sorting_label!,
this.hass.locale.language
this.hass?.locale.language ?? navigator.language
)
);
if (!sortedItems.length) {
sortedItems.push(
this._defaultNotFoundItem(this.notFoundLabel, this.hass.localize)
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
);
}
@@ -249,8 +257,20 @@ export class HaPickerComboBox extends LitElement {
const textfield = ev.target as HaTextField;
const searchString = textfield.value.trim();
if (!searchString) {
this._items = this._allItems;
return;
}
const index = this._fuseIndex(this._allItems);
const fuse = new HaFuse(this._allItems, { shouldSort: false }, index);
const fuse = new HaFuse(
this._allItems,
{
shouldSort: false,
minMatchCharLength: Math.min(searchString.length, 2),
},
index
);
const results = fuse.multiTermsSearch(searchString);
let filteredItems = this._allItems as PickerComboBoxItem[];
@@ -258,7 +278,7 @@ export class HaPickerComboBox extends LitElement {
const items = results.map((result) => result.item);
if (items.length === 0) {
items.push(
this._defaultNotFoundItem(this.notFoundLabel, this.hass.localize)
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
);
}
const additionalItems = this._getAdditionalItems(searchString);
@@ -431,6 +451,17 @@ export class HaPickerComboBox extends LitElement {
private _pickSelectedItem = (ev: KeyboardEvent) => {
ev.stopPropagation();
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
if (
this._virtualizerElement?.items.length === 1 &&
firstItem.id !== NO_MATCHING_ITEMS_FOUND_ID
) {
fireEvent(this, "value-changed", {
value: firstItem.id,
});
}
if (this._selectedItemIndex === -1) {
return;
}
@@ -438,7 +469,9 @@ export class HaPickerComboBox extends LitElement {
// if filter button is focused
ev.preventDefault();
const item: any = this._virtualizerElement?.items[this._selectedItemIndex];
const item = this._virtualizerElement?.items[
this._selectedItemIndex
] as PickerComboBoxItem;
if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) {
fireEvent(this, "value-changed", { value: item.id });
}

View File

@@ -1,122 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { BackgroundSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-picture-upload";
import "../ha-alert";
import type { HaPictureUpload } from "../ha-picture-upload";
import { URL_PREFIX } from "../../data/image_upload";
@customElement("ha-selector-background")
export class HaBackgroundSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value?: any;
@property({ attribute: false }) public selector!: BackgroundSelector;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@state() private yamlBackground = false;
protected updated(changedProps) {
super.updated(changedProps);
if (changedProps.has("value")) {
this.yamlBackground = !!this.value && !this.value.startsWith(URL_PREFIX);
}
}
protected render() {
return html`
<div>
${this.yamlBackground
? html`
<div class="value">
<img
src=${this.value}
alt=${this.hass.localize(
"ui.components.picture-upload.current_image_alt"
)}
/>
</div>
<ha-alert alert-type="info">
${this.hass.localize(
`ui.components.selectors.background.yaml_info`
)}
<ha-button slot="action" @click=${this._clearValue}>
${this.hass.localize(
`ui.components.picture-upload.clear_picture`
)}
</ha-button>
</ha-alert>
`
: html`
<ha-picture-upload
.hass=${this.hass}
.value=${this.value?.startsWith(URL_PREFIX) ? this.value : null}
.original=${!!this.selector.background?.original}
.cropOptions=${this.selector.background?.crop}
select-media
@change=${this._pictureChanged}
></ha-picture-upload>
`}
</div>
`;
}
private _pictureChanged(ev) {
const value = (ev.target as HaPictureUpload).value;
fireEvent(this, "value-changed", { value: value ?? undefined });
}
private _clearValue() {
fireEvent(this, "value-changed", { value: undefined });
}
static styles = css`
:host {
display: block;
position: relative;
}
ha-picture-upload {
background-color: var(--primary-background-color);
border-radius: var(--file-upload-image-border-radius);
}
div {
display: flex;
flex-direction: column;
}
ha-button {
white-space: nowrap;
--mdc-theme-primary: var(--primary-color);
}
.value {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
img {
max-width: 100%;
max-height: 200px;
margin-bottom: 4px;
border-radius: var(--file-upload-image-border-radius);
transition: opacity 0.3s;
opacity: var(--picture-opacity, 1);
}
img:hover {
opacity: 1;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-background": HaBackgroundSelector;
}
}

View File

@@ -34,7 +34,6 @@ const LOAD_ELEMENTS = {
file: () => import("./ha-selector-file"),
floor: () => import("./ha-selector-floor"),
label: () => import("./ha-selector-label"),
background: () => import("./ha-selector-background"),
language: () => import("./ha-selector-language"),
navigation: () => import("./ha-selector-navigation"),
number: () => import("./ha-selector-number"),

View File

@@ -76,11 +76,16 @@ class HaServicePicker extends LitElement {
</ha-combo-box-item>
`;
private _valueRenderer: PickerValueRenderer = (value) => {
private _valueRenderer = memoizeOne(
(
localize: LocalizeFunc,
services: HomeAssistant["services"]
): PickerValueRenderer =>
(value) => {
const serviceId = value;
const [domain, service] = serviceId.split(".");
if (!this.hass.services[domain]?.[service]) {
if (!services[domain]?.[service]) {
return html`
<ha-svg-icon slot="start" .path=${mdiRoomService}></ha-svg-icon>
<span slot="headline">${value}</span>
@@ -88,8 +93,8 @@ class HaServicePicker extends LitElement {
}
const serviceName =
this.hass.localize(`component.${domain}.services.${service}.name`) ||
this.hass.services[domain][service].name ||
localize(`component.${domain}.services.${service}.name`) ||
services[domain][service].name ||
service;
return html`
@@ -100,10 +105,13 @@ class HaServicePicker extends LitElement {
></ha-service-icon>
<span slot="headline">${serviceName}</span>
${this.showServiceId
? html`<span slot="supporting-text" class="code">${serviceId}</span>`
? html`<span slot="supporting-text" class="code"
>${serviceId}</span
>`
: nothing}
`;
};
}
);
protected render(): TemplateResult {
const placeholder =
@@ -123,7 +131,10 @@ class HaServicePicker extends LitElement {
.value=${this.value}
.getItems=${this._getItems}
.rowRenderer=${this._rowRenderer}
.valueRenderer=${this._valueRenderer}
.valueRenderer=${this._valueRenderer(
this.hass.localize,
this.hass.services
)}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>

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

@@ -214,8 +214,6 @@ export interface PipelineRun {
stage: "ready" | "wake_word" | "stt" | "intent" | "tts" | "done" | "error";
run: PipelineRunStartEvent["data"];
error?: PipelineErrorEvent["data"];
started: Date;
finished?: Date;
wake_word?: PipelineWakeWordStartEvent["data"] &
Partial<PipelineWakeWordEndEvent["data"]> & { done: boolean };
stt?: PipelineSTTStartEvent["data"] &
@@ -237,7 +235,6 @@ export const processEvent = (
stage: "ready",
run: event.data,
events: [event],
started: new Date(event.timestamp),
};
return run;
}
@@ -293,14 +290,9 @@ export const processEvent = (
tts: { ...run.tts!, ...event.data, done: true },
};
} else if (event.type === "run-end") {
run = { ...run, finished: new Date(event.timestamp), stage: "done" };
run = { ...run, stage: "done" };
} else if (event.type === "error") {
run = {
...run,
finished: new Date(event.timestamp),
stage: "error",
error: event.data,
};
run = { ...run, stage: "error", error: event.data };
} else {
run = { ...run };
}

View File

@@ -1,228 +0,0 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
export const enum ChatLogEventType {
INITIAL_STATE = "initial_state",
CREATED = "created",
UPDATED = "updated",
DELETED = "deleted",
CONTENT_ADDED = "content_added",
}
export interface ChatLogAttachment {
media_content_id: string;
mime_type: string;
path: string;
}
export interface ChatLogSystemContent {
role: "system";
content: string;
created: Date;
}
export interface ChatLogUserContent {
role: "user";
content: string;
created: Date;
attachments?: ChatLogAttachment[];
}
export interface ChatLogAssistantContent {
role: "assistant";
agent_id: string;
created: Date;
content?: string;
thinking_content?: string;
tool_calls?: any[];
}
export interface ChatLogToolResultContent {
role: "tool_result";
agent_id: string;
tool_call_id: string;
tool_name: string;
tool_result: any;
created: Date;
}
export type ChatLogContent =
| ChatLogSystemContent
| ChatLogUserContent
| ChatLogAssistantContent
| ChatLogToolResultContent;
export interface ChatLog {
conversation_id: string;
continue_conversation: boolean;
content: ChatLogContent[];
created: Date;
}
// Internal wire format types (not exported)
interface ChatLogSystemContentWire {
role: "system";
content: string;
created: string;
}
interface ChatLogUserContentWire {
role: "user";
content: string;
created: string;
attachments?: ChatLogAttachment[];
}
interface ChatLogAssistantContentWire {
role: "assistant";
agent_id: string;
created: string;
content?: string;
thinking_content?: string;
tool_calls?: {
tool_name: string;
tool_args: Record<string, any>;
id: string;
external: boolean;
}[];
}
interface ChatLogToolResultContentWire {
role: "tool_result";
agent_id: string;
tool_call_id: string;
tool_name: string;
tool_result: any;
created: string;
}
type ChatLogContentWire =
| ChatLogSystemContentWire
| ChatLogUserContentWire
| ChatLogAssistantContentWire
| ChatLogToolResultContentWire;
interface ChatLogWire {
conversation_id: string;
continue_conversation: boolean;
content: ChatLogContentWire[];
created: string;
}
const processContent = (content: ChatLogContentWire): ChatLogContent => ({
...content,
created: new Date(content.created),
});
const processChatLog = (chatLog: ChatLogWire): ChatLog => ({
...chatLog,
created: new Date(chatLog.created),
content: chatLog.content.map(processContent),
});
interface ChatLogInitialStateEvent {
event_type: ChatLogEventType.INITIAL_STATE;
data: ChatLogWire;
}
interface ChatLogIndexInitialStateEvent {
event_type: ChatLogEventType.INITIAL_STATE;
data: ChatLogWire[];
}
interface ChatLogCreatedEvent {
conversation_id: string;
event_type: ChatLogEventType.CREATED;
data: ChatLogWire;
}
interface ChatLogUpdatedEvent {
conversation_id: string;
event_type: ChatLogEventType.UPDATED;
data: { chat_log: ChatLogWire };
}
interface ChatLogDeletedEvent {
conversation_id: string;
event_type: ChatLogEventType.DELETED;
data: ChatLogWire;
}
interface ChatLogContentAddedEvent {
conversation_id: string;
event_type: ChatLogEventType.CONTENT_ADDED;
data: { content: ChatLogContentWire };
}
type ChatLogSubscriptionEvent =
| ChatLogInitialStateEvent
| ChatLogUpdatedEvent
| ChatLogDeletedEvent
| ChatLogContentAddedEvent;
type ChatLogIndexSubscriptionEvent =
| ChatLogIndexInitialStateEvent
| ChatLogCreatedEvent
| ChatLogDeletedEvent;
export const subscribeChatLog = (
hass: HomeAssistant,
conversationId: string,
callback: (chatLog: ChatLog | null) => void
): Promise<UnsubscribeFunc> => {
let chatLog: ChatLog | null = null;
return hass.connection.subscribeMessage<ChatLogSubscriptionEvent>(
(event) => {
if (event.event_type === ChatLogEventType.INITIAL_STATE) {
chatLog = processChatLog(event.data);
callback(chatLog);
} else if (event.event_type === ChatLogEventType.CONTENT_ADDED) {
if (chatLog) {
chatLog = {
...chatLog,
content: [...chatLog.content, processContent(event.data.content)],
};
callback(chatLog);
}
} else if (event.event_type === ChatLogEventType.UPDATED) {
chatLog = processChatLog(event.data.chat_log);
callback(chatLog);
} else if (event.event_type === ChatLogEventType.DELETED) {
chatLog = null;
callback(null);
}
},
{
type: "conversation/chat_log/subscribe",
conversation_id: conversationId,
}
);
};
export const subscribeChatLogIndex = (
hass: HomeAssistant,
callback: (chatLogs: ChatLog[]) => void
): Promise<UnsubscribeFunc> => {
let chatLogs: ChatLog[] = [];
return hass.connection.subscribeMessage<ChatLogIndexSubscriptionEvent>(
(event) => {
if (event.event_type === ChatLogEventType.INITIAL_STATE) {
chatLogs = event.data.map(processChatLog);
callback(chatLogs);
} else if (event.event_type === ChatLogEventType.CREATED) {
chatLogs = [...chatLogs, processChatLog(event.data)];
callback(chatLogs);
} else if (event.event_type === ChatLogEventType.DELETED) {
chatLogs = chatLogs.filter(
(chatLog) => chatLog.conversation_id !== event.conversation_id
);
callback(chatLogs);
}
},
{
type: "conversation/chat_log/subscribe_index",
}
);
};

View File

@@ -435,9 +435,9 @@ export const convertStatisticsToHistory = (
Object.entries(orderedStatistics).forEach(([key, value]) => {
const entityHistoryStates: EntityHistoryState[] = value.map((e) => ({
s: e.mean != null ? e.mean.toString() : e.state!.toString(),
lc: e.start / 1000,
lc: e.end / 1000,
a: {},
lu: e.start / 1000,
lu: e.end / 1000,
}));
statsHistoryStates[key] = entityHistoryStates;
});

View File

@@ -264,6 +264,7 @@ export const getLabels = (
const items = outputLabels.map<PickerComboBoxItem>((label) => ({
id: label.label_id,
primary: label.name,
secondary: label.description ?? "",
icon: label.icon || undefined,
icon_path: label.icon ? undefined : mdiLabel,
sorting_label: label.name,

View File

@@ -1,3 +1,4 @@
import type { MediaSelectorValue } from "../../selector";
import type { LovelaceBadgeConfig } from "./badge";
import type { LovelaceCardConfig } from "./card";
import type { LovelaceSectionRawConfig } from "./section";
@@ -8,7 +9,7 @@ export interface ShowViewConfig {
}
export interface LovelaceViewBackgroundConfig {
image?: string;
image?: string | MediaSelectorValue;
opacity?: number;
size?: "auto" | "cover" | "contain";
alignment?:

View File

@@ -5,7 +5,6 @@ import type {
import { ensureArray } from "../common/array/ensure-array";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { supportsFeature } from "../common/entity/supports-feature";
import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { isHelperDomain } from "../panels/config/helpers/const";
import type { UiAction } from "../panels/lovelace/components/hui-action-editor";
import type { HomeAssistant } from "../types";
@@ -47,8 +46,6 @@ export type Selector =
| FileSelector
| IconSelector
| LabelSelector
| ImageSelector
| BackgroundSelector
| LanguageSelector
| LocationSelector
| MediaSelector
@@ -273,14 +270,6 @@ export interface IconSelector {
} | null;
}
export interface ImageSelector {
image: { original?: boolean; crop?: CropOptions } | null;
}
export interface BackgroundSelector {
background: { original?: boolean; crop?: CropOptions } | null;
}
export interface LabelSelector {
label: {
multiple?: boolean;

View File

@@ -484,7 +484,7 @@ class DataEntryFlowDialog extends LitElement {
this._unsubDataEntryFlowProgress = undefined;
}
if (_step.next_flow[0] === "config_flow") {
showConfigFlowDialog(this._params!.dialogParentElement!, {
showConfigFlowDialog(this, {
continueFlowId: _step.next_flow[1],
carryOverDevices: this._devices(
this._params!.flowConfig.showDevices,
@@ -496,32 +496,23 @@ class DataEntryFlowDialog extends LitElement {
});
} else if (_step.next_flow[0] === "options_flow") {
if (_step.type === "create_entry") {
showOptionsFlowDialog(
this._params!.dialogParentElement!,
_step.result!,
{
showOptionsFlowDialog(this, _step.result!, {
continueFlowId: _step.next_flow[1],
navigateToResult: this._params!.navigateToResult,
dialogClosedCallback: this._params!.dialogClosedCallback,
}
);
});
}
} else if (_step.next_flow[0] === "config_subentries_flow") {
if (_step.type === "create_entry") {
showSubConfigFlowDialog(
this._params!.dialogParentElement!,
_step.result!,
_step.next_flow[0],
{
showSubConfigFlowDialog(this, _step.result!, _step.next_flow[0], {
continueFlowId: _step.next_flow[1],
navigateToResult: this._params!.navigateToResult,
dialogClosedCallback: this._params!.dialogClosedCallback,
}
);
});
}
} else {
this.closeDialog();
showAlertDialog(this._params!.dialogParentElement!, {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error",
{ error: `Unsupported next flow type: ${_step.next_flow[0]}` }

View File

@@ -678,8 +678,8 @@ export class MoreInfoDialog extends LitElement {
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
--vertical-align-dialog: flex-start;
--dialog-surface-margin-top: max(
40px,
var(--safe-area-inset-top, 0px)
var(--ha-space-10),
var(--safe-area-inset-top, var(--ha-space-0))
);
--dialog-content-padding: 0;
}
@@ -698,14 +698,15 @@ export class MoreInfoDialog extends LitElement {
}
ha-more-info-history-and-logbook {
padding: 8px 24px 24px 24px;
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-6)
var(--ha-space-6);
display: block;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
/* When in fullscreen dialog should be attached to top */
ha-dialog {
--dialog-surface-margin-top: 0px;
--dialog-surface-margin-top: var(--ha-space-0);
}
}
@@ -730,7 +731,8 @@ export class MoreInfoDialog extends LitElement {
display: flex;
flex-direction: column;
align-items: flex-start;
margin: 0 0 -10px 0;
margin: var(--ha-space-0) var(--ha-space-0)
calc(var(--ha-space-2) * -1) var(--ha-space-0);
}
.title p {
@@ -752,9 +754,9 @@ export class MoreInfoDialog extends LitElement {
font-size: var(--ha-font-size-m);
line-height: 16px;
--mdc-icon-size: 16px;
padding: 4px;
margin: -4px;
margin-top: -10px;
padding: var(--ha-space-1);
margin: calc(var(--ha-space-1) * -1);
margin-top: calc(var(--ha-space-2) * -1);
background: none;
border: none;
outline: none;

View File

@@ -1011,8 +1011,8 @@ export class QuickBar extends LitElement {
--mdc-dialog-max-width: 800px;
--mdc-dialog-min-width: 500px;
--dialog-surface-position: fixed;
--dialog-surface-top: 40px;
--mdc-dialog-max-height: calc(100% - 72px);
--dialog-surface-top: var(--ha-space-10);
--mdc-dialog-max-height: calc(100% - var(--ha-space-18));
}
}
@@ -1055,8 +1055,8 @@ export class QuickBar extends LitElement {
}
span.command-text {
margin-left: 8px;
margin-inline-start: 8px;
margin-left: var(--ha-space-2);
margin-inline-start: var(--ha-space-2);
margin-inline-end: initial;
direction: var(--direction);
}
@@ -1069,8 +1069,8 @@ export class QuickBar extends LitElement {
ha-md-list-item.two-line {
--md-list-item-one-line-container-height: 64px;
--md-list-item-two-line-container-height: 64px;
--md-list-item-top-space: 8px;
--md-list-item-bottom-space: 8px;
--md-list-item-top-space: var(--ha-space-2);
--md-list-item-bottom-space: var(--ha-space-2);
}
ha-md-list-item.three-line {
@@ -1078,8 +1078,8 @@ export class QuickBar extends LitElement {
--md-list-item-one-line-container-height: 72px;
--md-list-item-two-line-container-height: 72px;
--md-list-item-three-line-container-height: 72px;
--md-list-item-top-space: 8px;
--md-list-item-bottom-space: 8px;
--md-list-item-top-space: var(--ha-space-2);
--md-list-item-bottom-space: var(--ha-space-2);
}
ha-md-list-item .code {
@@ -1104,11 +1104,11 @@ export class QuickBar extends LitElement {
}
ha-tip {
padding: 20px;
padding: var(--ha-space-5);
}
.nothing-found {
padding: 16px 0px;
padding: var(--ha-space-4) var(--ha-space-0);
text-align: center;
}

View File

@@ -193,12 +193,12 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
).map(
(lang) =>
html`<ha-md-menu-item
.value=${lang.value}
.value=${lang.id}
@click=${this._handlePickLanguage}
@keydown=${this._handlePickLanguage}
.selected=${this._language === lang.value}
.selected=${this._language === lang.id}
>
${lang.label}
${lang.primary}
</ha-md-menu-item>`
)}
</ha-md-button-menu>`

View File

@@ -143,9 +143,14 @@ class DialogCalendarEventDetail extends LitElement {
this.hass.locale.time_zone,
this.hass.config.time_zone
);
const start = new TZDate(this._data!.dtstart, timeZone);
const endValue = new TZDate(this._data!.dtend, timeZone);
// All day events should be displayed as a day earlier
// For all-day events (date-only strings), parse without timezone to avoid offset issues
const start = isDate(this._data!.dtstart)
? new Date(this._data!.dtstart + "T00:00:00")
: new TZDate(this._data!.dtstart, timeZone);
const endValue = isDate(this._data!.dtend)
? new Date(this._data!.dtend + "T00:00:00")
: new TZDate(this._data!.dtend, timeZone);
// All day event end dates are exclusive in iCalendar format, subtract one day for display
const end = isDate(this._data.dtend) ? addDays(endValue, -1) : endValue;
// The range can be shortened when the start and end are on the same day.
if (isSameDay(start, end)) {

View File

@@ -15,6 +15,7 @@ import {
getFloors,
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
export interface ClimateViewStrategyConfig {
type: "climate";
@@ -152,6 +153,7 @@ export class ClimateViewStrategy extends ReactiveElement {
floorCount > 1
? floor.name
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
icon: floor.icon || floorDefaultIcon(floor),
},
],
};

View File

@@ -265,11 +265,8 @@ class DialogAreaDetail extends LitElement {
${this.hass.localize("ui.common.delete")}
</ha-button>`
: nothing}
<div slot="primaryAction">
<ha-button appearance="plain" @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${nameInvalid || !!this._submitting}
>
@@ -277,7 +274,6 @@ class DialogAreaDetail extends LitElement {
? this.hass.localize("ui.common.save")
: this.hass.localize("ui.common.create")}
</ha-button>
</div>
</ha-dialog>
`;
}

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ import { ensureArray } from "../../../../../common/array/ensure-array";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
import type { StateTrigger } from "../../../../../data/automation";
import { ANY_STATE_VALUE } from "../../../../../components/entity/const";
import type { HomeAssistant } from "../../../../../types";
import { baseTriggerStruct, forDictStruct } from "../../structs";
import type { TriggerElement } from "../ha-automation-trigger-row";
@@ -36,14 +37,12 @@ const stateTriggerStruct = assign(
trigger: literal("state"),
entity_id: optional(union([string(), array(string())])),
attribute: optional(string()),
from: optional(nullable(string())),
to: optional(nullable(string())),
from: optional(union([nullable(string()), array(string())])),
to: optional(union([nullable(string()), array(string())])),
for: optional(union([number(), string(), forDictStruct])),
})
);
const ANY_STATE_VALUE = "__ANY_STATE_IGNORE_ATTRIBUTES__";
@customElement("ha-automation-trigger-state")
export class HaStateTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -57,7 +56,12 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
}
private _schema = memoizeOne(
(localize: LocalizeFunc, attribute) =>
(
localize: LocalizeFunc,
attribute: string | undefined,
hideInFrom: string[],
hideInTo: string[]
) =>
[
{
name: "entity_id",
@@ -131,6 +135,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
},
selector: {
state: {
multiple: true,
extra_options: (attribute
? []
: [
@@ -142,6 +147,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
},
]) as any,
attribute: attribute,
hide_states: hideInFrom,
},
},
},
@@ -152,6 +158,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
},
selector: {
state: {
multiple: true,
extra_options: (attribute
? []
: [
@@ -163,6 +170,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
},
]) as any,
attribute: attribute,
hide_states: hideInTo,
},
},
},
@@ -207,13 +215,15 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
entity_id: ensureArray(this.trigger.entity_id),
for: trgFor,
};
if (!data.attribute && data.to === null) {
data.to = ANY_STATE_VALUE;
}
if (!data.attribute && data.from === null) {
data.from = ANY_STATE_VALUE;
}
const schema = this._schema(this.hass.localize, this.trigger.attribute);
data.to = this._normalizeStates(this.trigger.to, data.attribute);
data.from = this._normalizeStates(this.trigger.from, data.attribute);
const schema = this._schema(
this.hass.localize,
this.trigger.attribute,
data.to,
data.from
);
return html`
<ha-form
@@ -231,22 +241,60 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
ev.stopPropagation();
const newTrigger = ev.detail.value;
if (newTrigger.to === ANY_STATE_VALUE) {
newTrigger.to = newTrigger.attribute ? undefined : null;
newTrigger.to = this._applyAnyStateExclusive(
newTrigger.to,
newTrigger.attribute
);
if (Array.isArray(newTrigger.to) && newTrigger.to.length === 0) {
delete newTrigger.to;
}
if (newTrigger.from === ANY_STATE_VALUE) {
newTrigger.from = newTrigger.attribute ? undefined : null;
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) =>
newTrigger[key] === undefined || newTrigger[key] === ""
? delete newTrigger[key]
: {}
);
Object.keys(newTrigger).forEach((key) => {
const val = newTrigger[key];
if (val === undefined || val === "") {
delete newTrigger[key];
}
});
fireEvent(this, "value-changed", { value: newTrigger });
}
private _applyAnyStateExclusive(
val: string | string[] | null | undefined,
attribute?: string
): string | string[] | null | undefined {
const anyStateSelected = Array.isArray(val)
? val.includes(ANY_STATE_VALUE)
: val === ANY_STATE_VALUE;
if (anyStateSelected) {
// Any state is exclusive: null if no attribute, undefined if attribute
return attribute ? undefined : null;
}
return val;
}
private _normalizeStates(
value: string | string[] | null | undefined,
attribute?: string
): string[] {
// If no attribute is selected and backend value is null,
// expose it as the special ANY state option in the UI.
if (!attribute && value === null) {
return [ANY_STATE_VALUE];
}
if (value === undefined || value === null) {
return [];
}
return ensureArray(value);
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>

View File

@@ -98,7 +98,7 @@ class DialogCategoryDetail extends LitElement {
</div>
<ha-button
appearance="plain"
slot="primaryAction"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}

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

@@ -153,6 +153,8 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
},
yAxis: {
type: "value",
min: 0,
max: 100,
splitLine: {
show: true,
},

View File

@@ -6,7 +6,6 @@ import {
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { formatDateTimeWithSeconds } from "../../../../common/datetime/format_date_time";
import type {
PipelineRunEvent,
@@ -21,8 +20,6 @@ import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../types";
import "./assist-render-pipeline-events";
import type { ChatLog } from "../../../../data/chat_log";
import { subscribeChatLog } from "../../../../data/chat_log";
@customElement("assist-pipeline-debug")
export class AssistPipelineDebug extends LitElement {
@@ -40,12 +37,8 @@ export class AssistPipelineDebug extends LitElement {
@state() private _events?: PipelineRunEvent[];
@state() private _chatLog?: ChatLog;
private _unsubRefreshEventsID?: number;
private _unsubChatLogUpdates?: Promise<UnsubscribeFunc>;
protected render() {
return html`<hass-subpage
.narrow=${this.narrow}
@@ -113,7 +106,6 @@ export class AssistPipelineDebug extends LitElement {
? html`<assist-render-pipeline-events
.hass=${this.hass}
.events=${this._events}
.chatLog=${this._chatLog}
></assist-render-pipeline-events>`
: ""}
</div>
@@ -128,10 +120,6 @@ export class AssistPipelineDebug extends LitElement {
clearRefresh = true;
}
if (changedProperties.has("_runId")) {
if (this._unsubChatLogUpdates) {
this._unsubChatLogUpdates.then((unsub) => unsub());
this._unsubChatLogUpdates = undefined;
}
this._fetchEvents();
clearRefresh = true;
}
@@ -147,10 +135,6 @@ export class AssistPipelineDebug extends LitElement {
clearTimeout(this._unsubRefreshEventsID);
this._unsubRefreshEventsID = undefined;
}
if (this._unsubChatLogUpdates) {
this._unsubChatLogUpdates.then((unsub) => unsub());
this._unsubChatLogUpdates = undefined;
}
}
private async _fetchRuns() {
@@ -197,27 +181,8 @@ export class AssistPipelineDebug extends LitElement {
});
return;
}
if (!this._events!.length) {
return;
}
if (!this._unsubChatLogUpdates && this._events[0].type === "run-start") {
this._unsubChatLogUpdates = subscribeChatLog(
this.hass,
this._events[0].data.conversation_id,
(chatLog) => {
if (chatLog) {
this._chatLog = chatLog;
} else {
this._unsubChatLogUpdates?.then((unsub) => unsub());
this._unsubChatLogUpdates = undefined;
}
}
);
this._unsubChatLogUpdates.catch(() => {
this._unsubChatLogUpdates = undefined;
});
}
if (
this._events?.length &&
// If the last event is not a finish run event, the run is still ongoing.
// Refresh events automatically.
!["run-end", "error"].includes(this._events[this._events.length - 1].type)

View File

@@ -1,7 +1,6 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { extractSearchParam } from "../../../../common/url/search-params";
import "../../../../components/ha-assist-pipeline-picker";
import "../../../../components/ha-button";
@@ -25,8 +24,6 @@ import type { HomeAssistant } from "../../../../types";
import { AudioRecorder } from "../../../../util/audio-recorder";
import { fileDownload } from "../../../../util/file_download";
import "./assist-render-pipeline-run";
import type { ChatLog } from "../../../../data/chat_log";
import { subscribeChatLog } from "../../../../data/chat_log";
@customElement("assist-pipeline-run-debug")
export class AssistPipelineRunDebug extends LitElement {
@@ -49,13 +46,6 @@ export class AssistPipelineRunDebug extends LitElement {
@state() private _pipelineId?: string =
extractSearchParam("pipeline") || undefined;
@state() private _chatLog?: ChatLog;
private _chatLogSubscription: {
conversationId: string;
unsub: Promise<UnsubscribeFunc>;
} | null = null;
protected render(): TemplateResult {
return html`
<hass-subpage
@@ -188,7 +178,6 @@ export class AssistPipelineRunDebug extends LitElement {
<assist-render-pipeline-run
.hass=${this.hass}
.pipelineRun=${run}
.chatLog=${this._chatLog}
></assist-render-pipeline-run>
`
)}
@@ -197,14 +186,6 @@ export class AssistPipelineRunDebug extends LitElement {
`;
}
public disconnectedCallback(): void {
super.disconnectedCallback();
if (this._chatLogSubscription) {
this._chatLogSubscription.unsub.then((unsub) => unsub());
this._chatLogSubscription = null;
}
}
private get conversationId(): string | null {
return this._pipelineRuns.length === 0
? null
@@ -427,32 +408,6 @@ export class AssistPipelineRunDebug extends LitElement {
added = true;
}
callback(updatedRun);
const conversationId = this.conversationId;
if (
!this._chatLog &&
conversationId &&
(!this._chatLogSubscription ||
this._chatLogSubscription.conversationId !== conversationId)
) {
if (this._chatLogSubscription) {
this._chatLogSubscription.unsub.then((unsub) => unsub());
}
this._chatLogSubscription = {
conversationId,
unsub: subscribeChatLog(this.hass, conversationId, (chatLog) => {
if (chatLog) {
this._chatLog = chatLog;
} else {
this._chatLogSubscription?.unsub.then((unsub) => unsub());
this._chatLogSubscription = null;
}
}),
};
this._chatLogSubscription.unsub.catch(() => {
this._chatLogSubscription = null;
});
}
},
{
...options,

View File

@@ -9,7 +9,6 @@ import type {
import { processEvent } from "../../../../data/assist_pipeline";
import type { HomeAssistant } from "../../../../types";
import "./assist-render-pipeline-run";
import type { ChatLog } from "../../../../data/chat_log";
@customElement("assist-render-pipeline-events")
export class AssistPipelineEvents extends LitElement {
@@ -17,8 +16,6 @@ export class AssistPipelineEvents extends LitElement {
@property({ attribute: false }) public events!: PipelineRunEvent[];
@property({ attribute: false }) public chatLog?: ChatLog;
private _processEvents = memoizeOne(
(events: PipelineRunEvent[]): PipelineRun | undefined => {
let run: PipelineRun | undefined;
@@ -49,7 +46,6 @@ export class AssistPipelineEvents extends LitElement {
<assist-render-pipeline-run
.hass=${this.hass}
.pipelineRun=${run}
.chatLog=${this.chatLog}
></assist-render-pipeline-run>
`;
}

View File

@@ -1,5 +1,5 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-card";
import "../../../../components/ha-alert";
@@ -11,12 +11,6 @@ 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 {
ChatLogAssistantContent,
ChatLog,
ChatLogContent,
ChatLogUserContent,
} from "../../../../data/chat_log";
const RUN_DATA = {
pipeline: "Pipeline",
@@ -132,7 +126,7 @@ const dataMinusKeysRender = (
result[key] = data[key];
}
return render
? html`<ha-expansion-panel class="yaml-expansion">
? html`<ha-expansion-panel>
<span slot="header">Raw</span>
<ha-yaml-editor readOnly autoUpdate .value=${result}></ha-yaml-editor>
</ha-expansion-panel>`
@@ -145,8 +139,6 @@ export class AssistPipelineDebug extends LitElement {
@property({ attribute: false }) public pipelineRun!: PipelineRun;
@property({ attribute: false }) public chatLog?: ChatLog;
protected render(): TemplateResult {
const lastRunStage: string = this.pipelineRun
? ["tts", "intent", "stt", "wake_word"].find(
@@ -154,23 +146,8 @@ export class AssistPipelineDebug extends LitElement {
) || "ready"
: "ready";
let messages: ChatLogContent[];
const messages: { from: string; text: string }[] = [];
if (this.chatLog) {
messages = this.chatLog.content.filter(
this.pipelineRun.finished
? (content: ChatLogContent) =>
content.role === "system" ||
(content.created >= this.pipelineRun.started &&
content.created <= this.pipelineRun.finished!)
: (content: ChatLogContent) =>
content.role === "system" ||
content.created >= this.pipelineRun.started
);
} else {
messages = [];
// We don't have the chat log everywhere yet, just fallback for now.
const userMessage =
(this.pipelineRun.init_options &&
"text" in this.pipelineRun.init_options.input
@@ -181,20 +158,19 @@ export class AssistPipelineDebug extends LitElement {
if (userMessage) {
messages.push({
role: "user",
content: userMessage,
} as ChatLogUserContent);
from: "user",
text: userMessage,
});
}
if (
this.pipelineRun?.intent?.intent_output?.response?.speech?.plain?.speech
) {
messages.push({
role: "assistant",
content:
this.pipelineRun.intent.intent_output.response.speech.plain.speech,
} as ChatLogAssistantContent);
}
from: "hass",
text: this.pipelineRun.intent.intent_output.response.speech.plain
.speech,
});
}
return html`
@@ -209,57 +185,9 @@ export class AssistPipelineDebug extends LitElement {
${messages.length > 0
? html`
<div class="messages">
${messages.map((content) =>
content.role === "system" || content.role === "tool_result"
? html`
<ha-expansion-panel
class="content-expansion ${content.role}"
>
<div slot="header">
${content.role === "system"
? "System"
: `Result for ${content.tool_name}`}
</div>
${content.role === "system"
? html`<pre>${content.content}</pre>`
: html`
<ha-yaml-editor
read-only
auto-update
.value=${content}
></ha-yaml-editor>
`}
</ha-expansion-panel>
`
: html`
${content.content
? html`
<div class=${`message ${content.role}`}>
${content.content}
</div>
`
: nothing}
${content.role === "assistant" &&
content.tool_calls?.length
? html`
<ha-expansion-panel
class="content-expansion assistant"
>
<span slot="header">
Call
${content.tool_calls.length === 1
? content.tool_calls[0].tool_name
: `${content.tool_calls.length} tools`}
</span>
<ha-yaml-editor
read-only
auto-update
.value=${content.tool_calls}
></ha-yaml-editor>
</ha-expansion-panel>
`
: nothing}
${messages.map(
({ from, text }) => html`
<div class=${`message ${from}`}>${text}</div>
`
)}
</div>
@@ -432,7 +360,7 @@ export class AssistPipelineDebug extends LitElement {
: ""}
${maybeRenderError(this.pipelineRun, "tts", lastRunStage)}
<ha-card>
<ha-expansion-panel class="yaml-expansion">
<ha-expansion-panel>
<span slot="header">Raw</span>
<ha-yaml-editor
read-only
@@ -471,12 +399,12 @@ export class AssistPipelineDebug extends LitElement {
.row > div:last-child {
text-align: right;
}
.yaml-expansion {
ha-expansion-panel {
padding-left: 8px;
padding-inline-start: 8px;
padding-inline-end: initial;
}
.card-content .yaml-expansion {
.card-content ha-expansion-panel {
padding-left: 0px;
padding-inline-start: 0px;
padding-inline-end: initial;
@@ -492,59 +420,27 @@ export class AssistPipelineDebug extends LitElement {
margin-top: 8px;
}
.content-expansion {
margin: 8px 0;
border-radius: var(--ha-border-radius-xl);
clear: both;
padding: 0 8px;
--input-fill-color: none;
max-width: calc(100% - 24px);
--expansion-panel-summary-padding: 0px;
--expansion-panel-content-padding: 0px;
}
.content-expansion *[slot="header"] {
font-weight: var(--ha-font-weight-normal);
}
.system {
background-color: var(--success-color);
}
.message {
padding: 8px;
}
.message,
.content-expansion {
font-size: var(--ha-font-size-l);
margin: 8px 0;
padding: 8px;
border-radius: var(--ha-border-radius-xl);
clear: both;
}
.messages pre {
white-space: pre-wrap;
}
.user,
.tool_result {
.message.user {
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
float: var(--float-end);
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--light-primary-color);
color: var(--text-light-primary-color, var(--primary-text-color));
direction: var(--direction);
}
.message.user,
.content-expansion div[slot="header"] {
text-align: right;
}
.assistant {
.message.hass {
margin-right: 24px;
margin-inline-end: 24px;
margin-inline-start: initial;

View File

@@ -280,10 +280,11 @@ ${type === "object"
.content.horizontal {
--code-mirror-max-height: calc(
100vh - var(--header-height) - (var(--ha-line-height-normal) * 3) -
(1em * 2) - (max(16px, var(--safe-area-inset-top)) * 2) -
100vh - var(--header-height) -
(var(--ha-line-height-normal) * var(--ha-font-size-m) * 3) -
(max(16px, var(--safe-area-inset-top)) * 2) -
(max(16px, var(--safe-area-inset-bottom)) * 2) -
(var(--ha-card-border-width, 1px) * 2) - 179px
(var(--ha-card-border-width, 1px) * 3) - (1em * 2) - 192px
);
}

View File

@@ -313,9 +313,14 @@ class HaPanelHistory extends LitElement {
return;
}
const statsStartDate = new Date(this._startDate);
// History uses the end datapoint of the statistic, so if we want the
// graph to start at 7AM, need to fetch the statistic from 6AM.
statsStartDate.setHours(statsStartDate.getHours() - 1);
const statistics = await fetchStatistics(
this.hass!,
this._startDate,
statsStartDate,
this._endDate,
statisticIds,
"hour",

View File

@@ -5,6 +5,7 @@ import {
generateEntityFilter,
type EntityFilter,
} from "../../../common/entity/entity_filter";
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
@@ -98,6 +99,7 @@ export class LightViewStrategy extends ReactiveElement {
floorCount > 1
? floor.name
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
icon: floor.icon || floorDefaultIcon(floor),
},
],
};

View File

@@ -0,0 +1,126 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-number-buttons";
import { isUnavailableState } from "../../../data/entity";
import {
MediaPlayerEntityFeature,
type MediaPlayerEntity,
} from "../../../data/media-player";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
LovelaceCardFeatureContext,
MediaPlayerVolumeButtonsCardFeatureConfig,
} from "./types";
import { clamp } from "../../../common/number/clamp";
export const supportsMediaPlayerVolumeButtonsCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "media_player" &&
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET)
);
};
@customElement("hui-media-player-volume-buttons-card-feature")
class HuiMediaPlayerVolumeButtonsCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: MediaPlayerVolumeButtonsCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id] as
| MediaPlayerEntity
| undefined;
}
static getStubConfig(): MediaPlayerVolumeButtonsCardFeatureConfig {
return {
type: "media-player-volume-buttons",
step: 5,
};
}
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import(
"../editor/config-elements/hui-media-player-volume-buttons-card-feature-editor"
);
return document.createElement(
"hui-media-player-volume-buttons-card-feature-editor"
);
}
public setConfig(config: MediaPlayerVolumeButtonsCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsMediaPlayerVolumeButtonsCardFeature(this.hass, this.context)
) {
return nothing;
}
const position =
this._stateObj.attributes.volume_level != null
? Math.round(this._stateObj.attributes.volume_level * 100)
: undefined;
return html`
<ha-control-number-buttons
.disabled=${!this._stateObj || isUnavailableState(this._stateObj.state)}
.locale=${this.hass.locale}
min="0"
max="100"
.step=${this._config.step ?? 5}
.value=${position}
unit="%"
@value-changed=${this._valueChanged}
></ha-control-number-buttons>
`;
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
this.hass!.callService("media_player", "volume_set", {
entity_id: this._stateObj!.entity_id,
volume_level: clamp(ev.detail.value, 0, 100) / 100,
});
}
static get styles() {
return cardFeatureStyles;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-media-player-volume-buttons-card-feature": HuiMediaPlayerVolumeButtonsCardFeature;
}
}

View File

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

View File

@@ -50,6 +50,11 @@ export interface MediaPlayerVolumeSliderCardFeatureConfig {
type: "media-player-volume-slider";
}
export interface MediaPlayerVolumeButtonsCardFeatureConfig {
type: "media-player-volume-buttons";
step?: number;
}
export interface FanDirectionCardFeatureConfig {
type: "fan-direction";
}
@@ -252,6 +257,7 @@ export type LovelaceCardFeatureConfig =
| LockCommandsCardFeatureConfig
| LockOpenDoorCardFeatureConfig
| MediaPlayerPlaybackCardFeatureConfig
| MediaPlayerVolumeButtonsCardFeatureConfig
| MediaPlayerVolumeSliderCardFeatureConfig
| NumericInputCardFeatureConfig
| SelectOptionsCardFeatureConfig

View File

@@ -35,6 +35,8 @@ import { measureTextWidth } from "../../../../util/text";
import "../../../../components/ha-icon-button";
import { storage } from "../../../../common/decorators/storage";
import { listenMediaQuery } from "../../../../common/dom/media_query";
import { getEnergyColor } from "./common/color";
import type { CustomLegendOption } from "../../../../components/chart/ha-chart-base";
@customElement("hui-energy-devices-graph-card")
export class HuiEnergyDevicesGraphCard
@@ -49,6 +51,8 @@ export class HuiEnergyDevicesGraphCard
@state() private _data?: EnergyData;
@state() private _legendData: NonNullable<CustomLegendOption["data"]> = [];
@state()
@storage({
key: "energy-devices-graph-chart-type",
@@ -57,6 +61,14 @@ export class HuiEnergyDevicesGraphCard
})
private _chartType: "bar" | "pie" = "bar";
@state()
@storage({
key: "energy-devices-pie-hidden-stats",
state: true,
subscribe: false,
})
private _hiddenStats: string[] = [];
@state() private _isMobile = false;
private _compoundStats: string[] = [];
@@ -121,10 +133,16 @@ export class HuiEnergyDevicesGraphCard
<ha-chart-base
.hass=${this.hass}
.data=${this._chartData}
.options=${this._createOptions(this._chartData, this._chartType)}
.height=${`${Math.max(300, (this._chartData[0]?.data?.length || 0) * 28 + 50)}px`}
@chart-click=${this._handleChartClick}
.options=${this._createOptions(
this._chartData,
this._chartType,
this._legendData
)}
.height=${`${Math.max(300, (this._legendData?.length || 0) * 28 + 50)}px`}
.extraComponents=${[PieChart]}
@chart-click=${this._handleChartClick}
@dataset-hidden=${this._datasetHidden}
@dataset-unhidden=${this._datasetUnhidden}
></ha-chart-base>
</div>
</ha-card>
@@ -145,7 +163,8 @@ export class HuiEnergyDevicesGraphCard
private _createOptions = memoizeOne(
(
data: (BarSeriesOption | PieSeriesOption)[],
chartType: "bar" | "pie"
chartType: "bar" | "pie",
legendData: typeof this._legendData
): ECOption => {
const options: ECOption = {
grid: {
@@ -161,6 +180,7 @@ export class HuiEnergyDevicesGraphCard
},
xAxis: { show: false },
yAxis: { show: false },
legend: { type: "custom", show: false },
};
if (chartType === "bar") {
options.xAxis = {
@@ -191,6 +211,18 @@ export class HuiEnergyDevicesGraphCard
),
},
};
} else {
options.legend = {
type: "custom",
show: true,
data: legendData,
selected: legendData
.filter((d) => d.id && this._hiddenStats.includes(d.id))
.reduce((acc, d) => {
acc[d.id!] = false;
return acc;
}, {}),
};
}
return options;
}
@@ -354,23 +386,12 @@ export class HuiEnergyDevicesGraphCard
}
});
chartData.sort((a: any, b: any) => b.value[0] - a.value[0]);
if (compareData) {
datasets[1].data = chartData.map((d) =>
chartDataCompare.find((d2) => (d2 as any).id === d.id)
) as typeof chartDataCompare;
}
datasets.forEach((dataset) => {
dataset.data!.length = Math.min(
this._config?.max_devices || Infinity,
dataset.data!.length
);
});
if (this._chartType === "pie") {
const { summedData } = getSummedData(energyData);
const { consumption } = computeConsumptionData(summedData);
const { summedData, compareSummedData } = getSummedData(energyData);
const { consumption, compareConsumption } = computeConsumptionData(
summedData,
compareSummedData
);
const totalUsed = consumption.total.used_total;
const showUntracked =
"from_grid" in summedData ||
@@ -380,6 +401,47 @@ export class HuiEnergyDevicesGraphCard
? totalUsed -
chartData.reduce((acc: number, d: any) => acc + d.value[0], 0)
: 0;
if (untracked > 0) {
const color = getEnergyColor(
computedStyle,
this.hass.themes.darkMode,
false,
false,
"--history-unknown-color"
);
chartData.push({
id: "untracked",
value: [untracked, "untracked"] as any,
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.untracked_consumption"
),
itemStyle: {
color: color + "7F",
borderColor: color,
},
});
if (compareData) {
const compareUntracked =
compareConsumption!.total.used_total -
chartDataCompare.reduce(
(acc: number, d: any) => acc + d.value[0],
0
);
if (compareUntracked > 0) {
chartDataCompare.push({
id: "untracked",
value: [compareUntracked, "untracked"] as any,
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.untracked_consumption"
),
itemStyle: {
color: color + "32",
borderColor: color + "7F",
},
});
}
}
}
datasets.push({
type: "pie",
radius: ["0%", compareData ? "30%" : "40%"],
@@ -401,17 +463,36 @@ export class HuiEnergyDevicesGraphCard
color: "rgba(0, 0, 0, 0)",
},
tooltip: {
formatter: () =>
untracked > 0
? this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.includes_untracked",
{ num: formatNumber(untracked, this.hass.locale) }
)
: "",
show: false,
},
});
}
chartData.sort((a: any, b: any) => b.value[0] - a.value[0]);
if (
this._config?.max_devices &&
chartData.length > this._config.max_devices
) {
chartData.splice(this._config.max_devices);
}
this._legendData = chartData.map((d) => ({
...d,
name: this._getDeviceName(d.name),
}));
// filter out hidden stats in place
for (let i = chartData.length - 1; i >= 0; i--) {
if (this._hiddenStats.includes((chartData[i] as any).id)) {
chartData.splice(i, 1);
}
}
if (compareData) {
datasets[1].data = chartData.map((d) =>
chartDataCompare.find((d2) => (d2 as any).id === d.id)
) as typeof chartDataCompare;
}
this._chartData = datasets;
await this.updateComplete;
}
@@ -440,6 +521,18 @@ export class HuiEnergyDevicesGraphCard
this._getStatistics(this._data!);
}
private _datasetHidden(ev: CustomEvent<{ id: string }>) {
this._hiddenStats = [...this._hiddenStats, ev.detail.id];
this._getStatistics(this._data!);
}
private _datasetUnhidden(ev: CustomEvent<{ id: string }>) {
this._hiddenStats = this._hiddenStats.filter(
(stat) => stat !== ev.detail.id
);
this._getStatistics(this._data!);
}
static styles = css`
.card-header {
display: flex;

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

@@ -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() {
@@ -162,7 +180,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
private async _fetchStatistics(sensorNumericDeviceClasses: string[]) {
const now = new Date();
const start = new Date();
start.setHours(start.getHours() - this._hoursToShow);
start.setHours(start.getHours() - this._hoursToShow - 1);
const statistics = await fetchStatistics(
this.hass!,

View File

@@ -14,7 +14,11 @@ import { findEntities } from "../common/find-entities";
import { processConfigEntities } from "../common/process-config-entities";
import "../components/hui-warning";
import type { EntityConfig } from "../entity-rows/types";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type {
LovelaceCard,
LovelaceCardEditor,
LovelaceGridOptions,
} from "../types";
import type { LogbookCardConfig } from "./types";
import { resolveEntityIDs } from "../../../data/selector";
import { ensureArray } from "../../../common/array/ensure-array";
@@ -64,6 +68,15 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
return 9 + (this._config?.title ? 1 : 0);
}
public getGridOptions(): LovelaceGridOptions {
return {
rows: 6,
columns: 12,
min_columns: 6,
min_rows: this._config?.title ? 4 : 3,
};
}
public validateTarget(
config: LogbookCardConfig
): HassServiceTarget | undefined {
@@ -189,6 +202,10 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
>
<div class="content">
<ha-logbook
class=${classMap({
"is-grid": this.layout === "grid",
"is-panel": this.layout === "panel",
})}
.hass=${this.hass}
.time=${this._time}
.entityIds=${this._getEntityIds()}
@@ -212,6 +229,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
}
.content {
height: 100%;
padding: 0 16px 16px;
}
@@ -224,6 +242,11 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
display: block;
}
ha-logbook.is-grid,
ha-logbook.is-panel {
height: 100%;
}
:host([ispanel]) .content,
:host([ispanel]) ha-logbook {
height: 100%;

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

@@ -563,7 +563,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
.name,
.attribute {
font-size: var(--ha-font-size-m);
line-height: 1;
line-height: var(--ha-line-height-condensed);
}
.name-state {
@@ -729,7 +729,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
.short .state,
.short .temp-attribute .temp {
font-size: 24px;
line-height: 1.25;
line-height: var(--ha-line-height-condensed);
}
.short .content + .forecast {

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

View File

@@ -1,28 +1,29 @@
import type { HassEntity } from "home-assistant-js-websocket";
import {
DEFAULT_ENTITY_NAME,
type EntityNameItem,
} from "../../../../common/entity/compute_entity_name_display";
import type { HomeAssistant } from "../../../../types";
import { ensureArray } from "../../../../common/array/ensure-array";
import type { EntityNameItem } from "../../../../common/entity/compute_entity_name_display";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import type { HomeAssistant } from "../../../../types";
/**
* Computes the display name for an entity in Lovelace (cards and badges).
*
* @param hass - The Home Assistant instance
* @param stateObj - The entity state object
* @param nameConfig - The name configuration (string for override, or EntityNameItem[] for structured naming)
* @param config - The name configuration (string for override, or EntityNameItem[] for structured naming)
* @returns The computed entity name
*/
export const computeLovelaceEntityName = (
hass: HomeAssistant,
stateObj: HassEntity | undefined,
nameConfig: string | EntityNameItem | EntityNameItem[] | undefined
config: string | EntityNameItem | EntityNameItem[] | undefined
): string => {
if (typeof nameConfig === "string") {
return nameConfig;
// If no config is provided, fall back to the default state name
if (!config) {
return stateObj ? computeStateName(stateObj) : "";
}
if (typeof config === "string") {
return config;
}
const config = nameConfig || DEFAULT_ENTITY_NAME;
if (stateObj) {
return hass.formatEntityName(stateObj, config);
}

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

@@ -23,6 +23,7 @@ import "../card-features/hui-light-color-temp-card-feature";
import "../card-features/hui-lock-commands-card-feature";
import "../card-features/hui-lock-open-door-card-feature";
import "../card-features/hui-media-player-playback-card-feature";
import "../card-features/hui-media-player-volume-buttons-card-feature";
import "../card-features/hui-media-player-volume-slider-card-feature";
import "../card-features/hui-numeric-input-card-feature";
import "../card-features/hui-select-options-card-feature";
@@ -72,6 +73,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
"lock-commands",
"lock-open-door",
"media-player-playback",
"media-player-volume-buttons",
"media-player-volume-slider",
"numeric-input",
"select-options",

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

@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { array, assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
@@ -65,9 +64,7 @@ export class HuiAlarmPanelCardEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -5,7 +5,6 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
@@ -73,7 +72,7 @@ export class HuiButtonCardEditor
{
name: "name",
selector: {
entity_name: { default_name: DEFAULT_ENTITY_NAME },
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -48,6 +48,7 @@ import { supportsLightColorTempCardFeature } from "../../card-features/hui-light
import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature";
import { supportsMediaPlayerPlaybackCardFeature } from "../../card-features/hui-media-player-playback-card-feature";
import { supportsMediaPlayerVolumeButtonsCardFeature } from "../../card-features/hui-media-player-volume-buttons-card-feature";
import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature";
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
@@ -102,6 +103,7 @@ const UI_FEATURE_TYPES = [
"lock-commands",
"lock-open-door",
"media-player-playback",
"media-player-volume-buttons",
"media-player-volume-slider",
"numeric-input",
"select-options",
@@ -131,6 +133,7 @@ const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
"fan-preset-modes",
"humidifier-modes",
"lawn-mower-commands",
"media-player-volume-buttons",
"numeric-input",
"select-options",
"trend-graph",
@@ -171,6 +174,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
"lock-commands": supportsLockCommandsCardFeature,
"lock-open-door": supportsLockOpenDoorCardFeature,
"media-player-playback": supportsMediaPlayerPlaybackCardFeature,
"media-player-volume-buttons": supportsMediaPlayerVolumeButtonsCardFeature,
"media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature,
"numeric-input": supportsNumericInputCardFeature,
"select-options": supportsSelectOptionsCardFeature,

View File

@@ -13,7 +13,6 @@ import {
string,
union,
} from "superstruct";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
@@ -86,9 +85,7 @@ export class HuiEntityBadgeEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -1,5 +1,4 @@
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import { headerFooterConfigStructs } from "../../header-footer/structs";
@@ -26,9 +25,7 @@ const SCHEMA = [
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -14,7 +14,6 @@ import {
string,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import { NON_NUMERIC_ATTRIBUTES } from "../../../../data/entity_attributes";
@@ -102,9 +101,7 @@ export class HuiGaugeCardEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -46,10 +46,10 @@ export class HuiGenericEntityRowEditor
return [
{ name: "entity", required: true, selector: { entity: {} } },
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
name: "name",
selector: { entity_name: {} },
context: { entity: "entity" },
},
{
name: "icon",
selector: {
@@ -59,8 +59,6 @@ export class HuiGenericEntityRowEditor
icon_entity: "entity",
},
},
],
},
{
name: "secondary_info",
selector: {

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

@@ -14,7 +14,6 @@ import {
} from "superstruct";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type {
@@ -61,9 +60,7 @@ const SCHEMA = [
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -1,10 +1,9 @@
import { mdiGestureTap } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { mdiGestureTap } from "@mdi/js";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
@@ -37,9 +36,7 @@ const SCHEMA = [
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

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

@@ -2,7 +2,6 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
@@ -33,9 +32,7 @@ const SCHEMA = [
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -0,0 +1,86 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type {
LovelaceCardFeatureContext,
MediaPlayerVolumeButtonsCardFeatureConfig,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
@customElement("hui-media-player-volume-buttons-card-feature-editor")
export class HuiMediaPlayerVolumeButtonsCardFeatureEditor
extends LitElement
implements LovelaceCardFeatureEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: MediaPlayerVolumeButtonsCardFeatureConfig;
public setConfig(config: MediaPlayerVolumeButtonsCardFeatureConfig): void {
this._config = config;
}
private _schema = memoizeOne(
() =>
[
{
name: "step",
selector: {
number: {
mode: "slider",
step: 1,
min: 1,
max: 100,
unit_of_measurement: "%",
},
},
},
] as const
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const data: MediaPlayerVolumeButtonsCardFeatureConfig = {
type: "media-player-volume-buttons",
step: this._config.step ?? 5,
};
const schema = this._schema();
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) =>
this.hass!.localize(
`ui.panel.lovelace.editor.features.types.media-player-volume-buttons.${schema.name}`
);
}
declare global {
interface HTMLElementTagNameMap {
"hui-media-player-volume-buttons-card-feature-editor": HuiMediaPlayerVolumeButtonsCardFeatureEditor;
}
}

View File

@@ -1,8 +1,8 @@
import memoizeOne from "memoize-one";
import { mdiGestureTap } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
assert,
assign,
@@ -15,7 +15,6 @@ import {
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type {
@@ -71,9 +70,7 @@ export class HuiPictureEntityCardEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -2,7 +2,6 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
@@ -25,9 +24,7 @@ const SCHEMA = [
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -1,7 +1,7 @@
import memoizeOne from "memoize-one";
import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
assert,
assign,
@@ -12,18 +12,17 @@ import {
string,
union,
} from "superstruct";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-sensor-card";
import type { SensorCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import { configElementStyle } from "./config-elements-style";
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-sensor-card";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -71,9 +70,7 @@ export class HuiSensorCardEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

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

@@ -14,7 +14,7 @@ import {
} from "superstruct";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import { computeDomain } from "../../../../common/entity/compute_domain";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type {
@@ -35,7 +35,6 @@ import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor";
import type { FeatureType } from "./hui-card-features-editor";
import { computeDomain } from "../../../../common/entity/compute_domain";
const COMPATIBLE_FEATURES_TYPES: Record<string, FeatureType[]> = {
climate: [
@@ -89,9 +88,7 @@ export class HuiThermostatCardEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -16,7 +16,6 @@ import {
} from "superstruct";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { orderProperties } from "../../../../common/util/order-properties";
import "../../../../components/ha-expansion-panel";
@@ -102,9 +101,7 @@ export class HuiTileCardEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -12,7 +12,6 @@ import {
string,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
@@ -153,9 +152,7 @@ export class HuiWeatherForecastCardEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -13,7 +13,6 @@ import {
union,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
@@ -94,9 +93,7 @@ export class HuiHeadingEntityEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

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,11 +1,18 @@
import memoizeOne from "memoize-one";
import { LitElement, css, html, nothing } from "lit";
import type { PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import {
isMediaSourceContentId,
resolveMediaSource,
} from "../../../../data/media_source";
@customElement("hui-view-background-editor")
export class HuiViewBackgroundEditor extends LitElement {
@@ -13,6 +20,8 @@ export class HuiViewBackgroundEditor extends LitElement {
@state() private _config!: LovelaceViewConfig;
@state({ attribute: false }) private _resolvedImage?: string;
set config(config: LovelaceViewConfig) {
this._config = config;
}
@@ -20,10 +29,22 @@ export class HuiViewBackgroundEditor extends LitElement {
private _localizeValueCallback = (key: string) =>
this.hass.localize(key as any);
private _schema = memoizeOne((showSettings: boolean) => [
private _schema = memoizeOne(
(localize: LocalizeFunc, showSettings: boolean) =>
[
{
name: "image",
selector: { background: { original: true } },
selector: {
media: {
accept: ["image/*"] as string[],
clearable: true,
image_upload: true,
hide_content_type: true,
content_id_helper: localize(
"ui.panel.lovelace.editor.card.picture.content_id_helper"
),
},
},
},
...(showSettings
? ([
@@ -99,16 +120,73 @@ export class HuiViewBackgroundEditor extends LitElement {
},
] as const)
: []),
]);
] as const
);
protected updated(changedProps: PropertyValues) {
if (
this._config &&
this.hass &&
(changedProps.has("_config") ||
(changedProps.has("hass") && !changedProps.get("hass")))
) {
const background = this._backgroundData(this._config);
this.style.setProperty(
"--picture-opacity",
`${(background.opacity ?? 100) / 100}`
);
const backgroundImage =
typeof background.image === "object"
? background.image.media_content_id
: background.image;
if (backgroundImage && isMediaSourceContentId(backgroundImage)) {
resolveMediaSource(this.hass, backgroundImage).then((result) => {
this._resolvedImage = result.url;
});
} else {
this._resolvedImage = backgroundImage;
}
}
}
protected render() {
if (!this.hass) {
return nothing;
}
let background = this._config?.background;
const background = this._backgroundData(this._config);
return html`
${this._resolvedImage
? html`<div class="previewContainer">
<img
src=${this._resolvedImage}
alt=${this.hass.localize(
"ui.components.picture-upload.current_image_alt"
)}
/>
</div>`
: nothing}
<ha-form
.hass=${this.hass}
.data=${background}
.schema=${this._schema(this.hass.localize, true)}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
.localizeValue=${this._localizeValueCallback}
></ha-form>
`;
}
private _backgroundData = memoizeOne(
(backgroundConfig?: LovelaceViewConfig) => {
let background = backgroundConfig?.background;
if (typeof background === "string") {
const backgroundUrl = background.match(/url\(['"]?([^'"]+)['"]?\)/)?.[1];
const backgroundUrl = background.match(
/url\(['"]?([^'"]+)['"]?\)/
)?.[1];
background = {
image: backgroundUrl,
@@ -131,21 +209,14 @@ export class HuiViewBackgroundEditor extends LitElement {
repeat: "no-repeat",
attachment: "scroll",
...background,
...(typeof background.image === "string"
? { image: { media_content_id: background.image } }
: {}),
};
}
return html`
<ha-form
.hass=${this.hass}
.data=${background}
.schema=${this._schema(true)}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
.localizeValue=${this._localizeValueCallback}
style=${`--picture-opacity: ${(background.opacity ?? 100) / 100};`}
></ha-form>
`;
return background;
}
);
private _valueChanged(ev: CustomEvent): void {
const config = {
@@ -195,6 +266,23 @@ export class HuiViewBackgroundEditor extends LitElement {
display: block;
--file-upload-image-border-radius: var(--ha-border-radius-sm);
}
.previewContainer {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
img {
max-width: 100%;
max-height: 200px;
margin-bottom: 4px;
border-radius: var(--file-upload-image-border-radius);
transition: opacity 0.3s;
opacity: var(--picture-opacity, 1);
}
img:hover {
opacity: 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

Some files were not shown because too many files have changed in this diff Show More