Compare commits

..
Author SHA1 Message Date
Aidan Timson d1afcfcd32 Match original more closely 2025-11-07 13:43:20 +00:00
Aidan Timson 29e6ada90e Remove binary 2025-11-07 12:12:32 +00:00
Aidan Timson ff1745fccc Working connection 2025-11-07 12:11:04 +00:00
Aidan Timson 59aaf86104 Update 2025-11-07 11:54:59 +00:00
Aidan Timson 52bc692c79 Add simple backend 2025-11-07 11:45:51 +00:00
Aidan Timson 29de405912 Setup CLI in container 2025-11-07 11:37:58 +00:00
Aidan Timson a723171ef2 Clear mocks 2025-11-07 11:25:36 +00:00
Aidan Timson cfe193cf60 Setup 2025-11-07 11:16:04 +00:00
Aidan Timson a8e6557b09 Fix 2025-11-07 10:49:23 +00:00
Aidan Timson a517aabdd8 Basic auto theme support 2025-11-07 10:44:30 +00:00
Aidan Timson 10c8828aa5 Setup 2025-11-07 10:33:51 +00:00
Aidan TimsonandGitHub 5801ce944c Refactor dashboard conditional listeners to use mixin (#27837)
* Update media query to new APIs

* Refactor conditional rendering

* Cleanup

* Restore original functionality

* Restore

* Reduce

* Reduce

* Reduce

* Restore legacy code while we still support them

* Clear conditional listeners before setting up again
2025-11-07 12:22:16 +02:00
Petar PetrovandGitHub 79ad9dbf44 Disable graph resize animation for general resizing (#27816) 2025-11-07 08:34:42 +01:00
WendelinandGitHub 9814533d47 Add service titles to automation action sidebar (#27831)
* Add service titles to automation action sidebar

* Add split limit
2025-11-07 07:27:21 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
bdb6c684c0 Update dependency eslint to v9.39.1 (#27846)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-07 08:38:42 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
0046167f5c Update dependency typescript-eslint to v8.46.3 (#27842)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-07 08:38:21 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5266f0d761 Update dependency @bundle-stats/plugin-webpack-filter to v4.21.6 (#27841)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-07 08:37:35 +02:00
Jan BouwhuisandGitHub cc5c0d04c4 Fix index for service action translation in service action dialog (#27824) 2025-11-06 15:15:52 +01:00
WendelinandGitHub 7f3d5f557f Target picker row check if not found entity isn't "all" (#27826)
Target picker row check if not found entity isn't all
2025-11-06 15:15:09 +01:00
9b48cd7737 Add trigger/condition/action dialog: select single search result with enter key (#27825)
* Add trigger/condition/action dialog: select single search result with enter key

* Update src/panels/config/automation/add-automation-element-dialog.ts

---------

Co-authored-by: Petar Petrov <[email protected]>
2025-11-06 13:46:53 +00:00
Aidan TimsonandGitHub 11c6f6ec78 Update @home-assistant/webawesome to 3.0.0-beta.6.ha.7 (#27834) 2025-11-06 15:33:10 +02:00
WendelinandGitHub cb0f59b26d Fix floor details area picker (#27827) 2025-11-06 12:42:30 +02:00
Paul BotteinandGitHub c89fc35578 Fix OHF logo theme (#27830) 2025-11-06 12:39:52 +02:00
TimothyandGitHub f03cd9c239 Add Add entity to feature for external_app (#26346)
* Add Add entity to feature for external_app

* Update icon from plus to plusboxmultiple

* Apply suggestion on the name

* Add missing shouldHandleRequestSelectedEvent that caused duplicate

* WIP

* Rework the logic to match the agreed design

* Rename property

* Apply PR comments

* Apply prettier

* Merge MessageWithAnswer

* Apply PR comments
2025-11-06 08:25:05 +00:00
karwostsandGitHub 19a4e37933 Fix incorrect unit displayed in energy grid flow settings (#27822) 2025-11-06 08:46:19 +02:00
Bram KragtenandGitHub 76514babd5 Fix landing page build (#27817) 2025-11-05 16:13:48 +01:00
TimothyandGitHub b6abbdafb8 All external config properties could be undefined (#27803)
All external config attributes could be undefined
2025-11-05 16:57:51 +02:00
WendelinandGitHub d35e6c0092 Add fallback icon for domain template (#27814) 2025-11-05 15:14:41 +01:00
WendelinandGitHub 5c2ee54dec Fix target picker with empty sections (#27813) 2025-11-05 13:56:58 +00:00
WendelinandGitHub 1dfca76c81 Fix assist conversation language picker (#27764) 2025-11-05 14:48:18 +01:00
WendelinandGitHub fd7f028fbf Change add trigger/condition/action dialog title (#27811)
Change add dialog title
2025-11-05 15:31:25 +02:00
WendelinandGitHub 3f7283b1af Add trigger/condition/action dialog - Show device group always on top (#27812)
add automation element dialog Device always on top
2025-11-05 15:27:53 +02:00
d35323ac52 Fix target picker in logbook card editor (#27804)
Co-authored-by: Wendelin <[email protected]>
2025-11-05 11:57:34 +00:00
WendelinandGitHub 06475382e8 Fix auth language picker styles (#27805) 2025-11-05 10:54:36 +01:00
WendelinandGitHub b60dd7f15d Add condition/action dialog: blocks title (#27801) 2025-11-05 10:47:46 +01:00
WendelinandGitHub b77e65fabd Add trigger/condition/action dialog: fix empty elements in search results (#27802) 2025-11-05 10:47:20 +01:00
WendelinandGitHub cea691a04e Fix target picker in card editor (#27800) 2025-11-05 10:03:56 +01:00
Jan-Philipp BeneckeandGitHub 50df2a34cd Fix z-index for target picker item row icon (#27798) 2025-11-05 08:33:11 +01:00
Aidan TimsonandGitHub e6c0a84994 Add hide background option to iframe card (#27792)
* Add hide background option to iframe card

* Fix

* Add helper
2025-11-05 08:07:43 +02:00
Bram KragtenandGitHub b03fa4bdc5 Handle unknown items in target picker (#27795)
* Handle unknown items in target picker

* Update ha-target-picker-item-row.ts

* update colors

* fallback to domain icons
2025-11-04 18:02:56 +01:00
Paul BotteinandGitHub 058cecc124 Auto refresh summary dashboard when registries changed (#27794) 2025-11-04 16:41:27 +00:00
Paul BotteinandGitHub a5f058a7eb Rename safety panel to security panel (#27796) 2025-11-04 17:23:06 +01:00
Paul BotteinandGitHub 655c2ff3c2 Don't show summary card if summary dashboards are empty (#27788)
Don't show summary card if summary dashboard are empty
2025-11-04 13:40:53 +02:00
Tobias BieniekandGitHub e1b0a3e737 Hide media players summary when no entities exist (#27642) 2025-11-04 11:44:12 +01:00
Paul BotteinandGitHub d59d436080 Display entities without area in summary dashboard (#27777)
* Add support for no area, no floor and no device in entity filter

* Display entities without area in summary dashboard
2025-11-04 12:10:18 +02:00
0fe0bf12f2 Fix-labels-yaml-helper (#27776)
Co-authored-by: Bram Kragten <[email protected]>
2025-11-04 09:01:36 +00:00
Paul BotteinandGitHub f5a3877f47 Fix tooltip hide delay (#27786) 2025-11-04 09:45:26 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
31d04f5338 Update dependency globals to v16.5.0 (#27785)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 08:49:20 +02:00
karwostsandGitHub 4f7d223aa7 Fix sankey with external statistics devices (#27784) 2025-11-04 08:22:36 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
484c60073d Update dependency @octokit/rest to v22.0.1 (#27779)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 06:33:20 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
0e1ab1a60c Update dependency hls.js to v1.6.14 (#27780)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 06:33:05 +01:00
Petar PetrovandGitHub cef11e0c18 Apply theme variables to pi charts (#27773) 2025-11-03 16:27:09 +01:00
55e75e80d2 Fixes in backup overflow (#27745)
Co-authored-by: Bram Kragten <[email protected]>
2025-11-03 16:26:42 +01:00
Petar PetrovandGitHub 126a78ec8a Fix sensor card graph in Safari (#27768) 2025-11-03 16:00:06 +01:00
Petar PetrovandGitHub 063af39f0f Fix for Y axis label formatting in history graph (#27770) 2025-11-03 15:59:46 +01:00
WendelinandGitHub 132c4c8201 Revert "Show action description in sidebar header using describeAction" (#27772) 2025-11-03 15:59:03 +01:00
Aidan TimsonandGitHub 4c08e960f1 Use supervisor endpoint for downloading logs (when avaliable) (#27765) 2025-11-03 15:58:29 +01:00
WendelinandGitHub a8020256de Fix selected element text color (#27771) 2025-11-03 16:57:01 +02:00
Petar PetrovandGitHub 2ea57c33ae Remove dynamic eventDisplay in calendar (#27767) 2025-11-03 16:33:11 +02:00
Paul BotteinandGitHub db1408666c Don't show tooltip on overflow menu in dashboard toolbar (#27763) 2025-11-03 14:30:04 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
260288a061 Update vitest monorepo to v4.0.6 (#27766)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 15:14:01 +02:00
Ezra FreedmanandGitHub 45fd685913 Fix display of multi-day events on calendar card view (#27730)
* Fix calendar card multi-day events not spanning in month view

* not used for fix
2025-11-03 15:03:16 +02:00
Ezra FreedmanandGitHub 896d76b218 Fix calendar all-day toggle date normalization (#27701) 2025-11-03 14:55:20 +02:00
Petar PetrovandGitHub cec24117dc Auto update statistics graph in more-info (#27760) 2025-11-03 14:02:15 +02:00
34006d268b Translate voice assistant pipeline debugger (#27721)
* Translate voice assistant pipeline debugger

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

Co-authored-by: Paul Bottein <[email protected]>

* Typo

* Use keys in render functions

---------

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

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

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

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

* Migrate statistic card

* Fix localize
2025-10-30 16:58:52 +01:00
Paul BotteinandGitHub f1c9802ee3 Revert entity naming in target picker chips (#27722) 2025-10-30 16:24:09 +01:00
Aidan TimsonandGitHub 35697e3f94 Calendar card height: account for title and stop overflow (#27707) 2025-10-30 14:47:27 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8ea7ad3026 Update vitest monorepo to v4.0.4 (#27720)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 15:59:03 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
73747fbedc Update dependency @rsdoctor/rspack-plugin to v1.3.6 (#27719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 13:57:36 +00:00
Aidan TimsonandGitHub aaa92bd354 Assist pipelines: allow user to stop TTS audio test (#27710)
* Allow user to stop TTS audio test

* Change button color on state

* Use getter over state
2025-10-30 15:35:19 +02:00
Aidan TimsonandGitHub 5f75fc5bcb Revert "Migrate dialog-device-registry-detail to ha-wa-dialog (#27668)" (#27716)
This reverts commit 2a8d935601.
2025-10-30 12:12:23 +00:00
karwostsandGitHub 5fa44548c3 Add a state filter to logbook card (#27685)
* Add a state filter to logbook card

* types
2025-10-30 12:23:29 +02:00
Aidan TimsonandGitHub 1945c11621 Trend feature: make sure content is centered when loading (#27708)
* Make sure content is centered when loading

* Restore from test
2025-10-30 09:43:04 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
930575d292 Update dependency @rsdoctor/rspack-plugin to v1.3.5 (#27706)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 06:02:18 +00:00
Paul BotteinandGitHub 0147dbab00 Restore trigger id in overflow menu for trigger (#27702) 2025-10-30 04:26:25 +02:00
Paul BotteinandGitHub 13ace24b83 Only display add button if at least one entity is selected in entities picker (#27699) 2025-10-29 17:05:34 +01:00
Paul BotteinandGitHub 616333591a Only clear from and to trigger in state trigger (#27700) 2025-10-29 17:02:02 +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
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 <[email protected]>

* resize fix

* some fixes

---------

Co-authored-by: Paul Bottein <[email protected]>
2025-10-29 14:50:57 +00:00
Ezra FreedmanandGitHub 7b188759e3 Fix todo item date picker displaying previous day (#27698) 2025-10-29 14:21:34 +00:00
76772d1098 Default card name to friendly name (#27696)
Co-authored-by: Bram Kragten <[email protected]>
2025-10-29 13:41:45 +00:00
Paul BotteinandGitHub 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 BieniekandGitHub 52eb3d8063 Add automations category to home dashboard area views (#27641) 2025-10-29 12:21:17 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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 BieniekandGitHub 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 PetrovandGitHub c0cc66c1ab Fix next flow config flow showing an empty dialog (#27682) 2025-10-29 09:53:14 +01:00
ildar170975andGitHub 7cfbc521c7 Dev tools -> Templates: max-height fix for cm-editor (#27461) 2025-10-29 09:52:45 +01:00
Ezra FreedmanandGitHub e064ce56cc Fix calendar all day date display (#27689) 2025-10-29 09:42:50 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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
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 <[email protected]>

* Format

* Remove entities length check

---------

Co-authored-by: Petar Petrov <[email protected]>
2025-10-28 20:35:55 +01:00
Aidan TimsonandGitHub 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
karwostsandGitHub c88f8fcce0 Shift stats in history by 1 hour (#27633) 2025-10-28 15:53:22 +02:00
Tobias BieniekandGitHub 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 WagnerandGitHub 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
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 <[email protected]>
2025-10-28 13:47:11 +02:00
Aidan TimsonandGitHub a545a377a7 Fix typos and improve grammar on ha-dialogs design docs (#27681) 2025-10-28 12:38:47 +01:00
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 <[email protected]>

---------

Co-authored-by: Petar Petrov <[email protected]>
2025-10-28 13:29:56 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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
453a2ac7f3 Use generic picker for language picker (#27631)
Co-authored-by: Bram Kragten <[email protected]>
2025-10-27 14:02:13 +00:00
karwostsandGitHub 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 TimsonandGitHub 2a8d935601 Migrate dialog-device-registry-detail to ha-wa-dialog (#27668) 2025-10-27 14:30:22 +02:00
Aidan TimsonandGitHub a6328fb6d7 Use space tokens in ha-more-info-dialog (#27666) 2025-10-27 13:44:49 +02:00
Aidan TimsonandGitHub a78b61006f Use space tokens in ha-automation-row (#27665) 2025-10-27 13:40:37 +02:00
Aidan TimsonandGitHub d506aa23b6 Use space tokens in ha-markdown (#27664) 2025-10-27 13:40:04 +02:00
Aidan TimsonandGitHub 48b4df43ab Use space tokens in ha-quick-bar (#27663) 2025-10-27 13:39:41 +02:00
Aidan TimsonandGitHub 8cdcd9cb55 Use space tokens in ha-card (#27662) 2025-10-27 13:38:36 +02:00
ildar170975andGitHub 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 LamonandGitHub 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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...330a01c490aca151604b8cf639adc76d48f6c5d4)

---
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] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 10:49:12 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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 LamonandGitHub 8bbd7a6a06 Add min and max values for hardware graphs (#27649) 2025-10-27 08:31:24 +02:00
karwostsandGitHub 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 LamonandGitHub 9943dae82c Adjust line height in weather card (#27653) 2025-10-27 08:29:17 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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](https://github.com/github/codeql-action/compare/16140ae1a102900babc80a33c44059580f687047...4e94bd11f71e507f7f87df81788dff88d1dacbfb)

---
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] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 08:27:30 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
164ec2a9b5 Update vitest monorepo to v4 (major) (#27638)
Update vitest monorepo to v4

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-26 15:34:28 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
20001a551c Update CodeMirror (#27647)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-26 15:28:28 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
b7f85bf733 Update dependency @rsdoctor/rspack-plugin to v1.3.4 (#27648)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-26 15:28:00 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
b303e9441b Update dependency lint-staged to v16.2.6 (#27644)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-26 07:19:35 +02:00
ildar170975andGitHub 8f4bd0f620 Fix horiz spacings in ha-select (#27634)
* fix horiz paddings

* prettier

* prettier
2025-10-25 13:37:25 +03:00
Petar PetrovandGitHub 596346bf59 Improve label layout for pie chart on mobile (#27632) 2025-10-24 16:34:38 +02:00
Paul BotteinandGitHub 769cea92aa Don't force ratio for area picture (#27630) 2025-10-24 16:12:14 +03:00
WendelinandGitHub f825016514 Add color and icon defaults for config labels table (#27622)
* Add color and icon defaults for config labels table

* use space vars
2025-10-24 14:00:06 +02:00
Aidan TimsonandGitHub c6fd45bd6a Use space tokens in card features editor (#27625) 2025-10-24 13:34:10 +02:00
Aidan TimsonandGitHub 6c4f4af75c Use space tokens in tile card (#27626) 2025-10-24 13:33:54 +02:00
Aidan TimsonandGitHub cd5c3ef2f6 Use space tokens in area card (#27627) 2025-10-24 13:33:39 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
636a6fa02e Bump vite from 7.1.6 to 7.1.12 (#27628)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.6 to 7.1.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.1.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-24 13:17:37 +02:00
21b83426d6 Migrate generic-picker to new design (#27594)
* WIP new combo box

* Use new combo box for generic picker

* Fix esc close and clean up

* Fix empty search list item

* Fix picker usages

* Improve labels picker

* Patch WA to make esc on popover work correctly

* Apply suggestion from @piitaya

Co-authored-by: Paul Bottein <[email protected]>

* Fix NO_MATCHING_ITEMS_FOUND_ID

* Fix possible undefined boolean props

---------

Co-authored-by: Paul Bottein <[email protected]>
2025-10-24 12:52:56 +02:00
c139ec22f9 Use space vars (#27623)
Co-authored-by: Aidan Timson <[email protected]>
2025-10-24 09:43:49 +00:00
ildar170975andGitHub a6ef3a26da Fix padding for "search-input-outlined" in filters (#27621) 2025-10-24 09:41:50 +00:00
WendelinandGitHub 221ca56121 Add automation element dialog: fix blocks only search result (#27618)
Fix exact block search
2025-10-24 11:25:39 +03:00
ildar170975andGitHub 4e6e3629a8 target picker: use slugify() for tooltips (#27619) 2025-10-24 08:59:14 +01:00
ildar170975andGitHub fe94ae0243 ha-media-player-browse: use slugify() for tooltips (#27617) 2025-10-24 09:18:10 +02:00
8a1a22d4bd New design for automation add trigger/condition/action dialog (#27529)
* 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

* fix translations, scroll issues, RTL

---------

Co-authored-by: Petar Petrov <[email protected]>
2025-10-24 05:53:19 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
153a578986 Update dependency typescript-eslint to v8.46.2 (#27610)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 21:18:34 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
04bb10d0a2 Update dependency lint-staged to v16.2.5 (#27609)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 21:18:20 +02:00
Bram KragtenandGitHub 35e52de2c1 Handle service description might be undefined (#27606) 2025-10-23 21:15:43 +02:00
Petar PetrovandGitHub b0862fddaa Fix resizing in pie chart (#27608) 2025-10-23 17:39:43 +03:00
WendelinandGitHub 77735f5310 Fix automation sidebar editor rerendering (#27607) 2025-10-23 16:06:53 +02:00
WendelinandGitHub e388756533 ha-wa-dialog show header border on scroll (#27605) 2025-10-23 15:55:46 +02:00
WendelinandGitHub e9ca9bb781 Target picker fix entities count for labels (#27603) 2025-10-23 15:53:48 +02:00
Tobias BieniekandGitHub e48918442c Invert floor sort order to match physical layout (#27580) 2025-10-23 15:39:58 +02:00
WendelinandGitHub 52f37f41f0 Revert "Sidebar profile picture fix alignment in RTL languages" (#27604) 2025-10-23 14:31:59 +01:00
Paul BotteinandGitHub 4687006fec Add description support to fields in object selector (#27602)
* Add description support to fields in object selector

* Use object

* Rename helper to description
2025-10-23 12:23:28 +00:00
Paul BotteinandGitHub aca4ca3066 Align state content picker with entity name picker (#27530)
* Align state content picker with entity name picker

* Fix state content property
2025-10-23 15:16:56 +03:00
WendelinandGitHub 3a2c00622a Sidebar profile picture fix alignment in RTL languages (#27578)
* Fix floor entities count

* Fix rtl profile picture
2025-10-23 15:09:15 +03:00
WendelinandGitHub 699c25a6c3 Show less information in picked targets (#27600)
Do not show num device and entity domain
2025-10-23 09:55:51 +02:00
ildar170975andGitHub 1ad226d608 Fix min/max-height in error-log-card to prevent extra scrollbar (#27591) 2025-10-23 09:11:56 +02:00
karwostsandGitHub 992a4cd98a Improve automation save timeout (#27584)
* Improve automation save timeout

* junk

* fix error handling

* fix error handling

* translate fix

* Fix typo
2025-10-23 08:59:30 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
fd217f8ea5 Update dependency eslint-plugin-unused-imports to v4.3.0 (#27598)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 08:49:28 +03:00
WendelinandGitHub dede14e578 Use ha-filter-chips for target picker (#27521)
* Use filter-chips for target picker

* Single select filter

* fix filter radius
2025-10-22 21:08:30 +02:00
ildar170975andGitHub fa7aca67e5 codemirror: show a cursor while drag-n-drop (#27592)
* add dropCursor

* add dropCursor
2025-10-22 13:25:47 +02:00
karwostsandGitHub 6abdfa6d5c Set numeric keypads to LTR (#27588) 2025-10-22 09:43:56 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
0a70e2abda Update dependency eslint to v9.38.0 (#27582)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 12:50:25 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
1ec589e9b6 Update Node.js to v22.21.0 (#27583)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 12:50:05 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2d2b5633c4 Update dependency jsdom to v27.0.1 (#27586)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 12:49:46 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Petar Petrov
76df75c306 Bump actions/setup-node from 5.0.0 to 6.0.0 (#27568)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/a0853c24544627f65ddf259abe73b1d18a591444...2028fbc5c25fe9cf00d9f06a71cc4710d4507903)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <[email protected]>
2025-10-20 11:14:56 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Petar Petrov
027ded61c2 Bump home-assistant/wheels from 2025.09.1 to 2025.10.0 (#27566)
Bumps [home-assistant/wheels](https://github.com/home-assistant/wheels) from 2025.09.1 to 2025.10.0.
- [Release notes](https://github.com/home-assistant/wheels/releases)
- [Commits](https://github.com/home-assistant/wheels/compare/2025.09.1...2025.10.0)

---
updated-dependencies:
- dependency-name: home-assistant/wheels
  dependency-version: 2025.10.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <[email protected]>
2025-10-20 13:14:25 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Petar Petrov
a718589ba0 Bump relative-ci/agent-action from 3.0.1 to 3.1.0 (#27569)
Bumps [relative-ci/agent-action](https://github.com/relative-ci/agent-action) from 3.0.1 to 3.1.0.
- [Release notes](https://github.com/relative-ci/agent-action/releases)
- [Commits](https://github.com/relative-ci/agent-action/compare/1707825cbfcc7452b2913d273414705415ae64d4...8504826a02078b05756e4c07e380023cc2c4274a)

---
updated-dependencies:
- dependency-name: relative-ci/agent-action
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <[email protected]>
2025-10-20 11:14:02 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5b5dc9d853 Lock file maintenance (#27560)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 11:08:09 +00:00
WendelinandGitHub 2a49b5e15a Fix target-picker floor entities count (#27577)
Fix floor entities count
2025-10-20 14:05:37 +03:00
fa4dd1c5ea Make target area of slider track larger (#27571)
Co-authored-by: Petar Petrov <[email protected]>
2025-10-20 11:02:12 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Petar Petrov
37a3af2e8b Bump github/codeql-action from 4.30.8 to 4.30.9 (#27567)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.30.8 to 4.30.9.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/f443b600d91635bebf5b0d9ebc620189c0d6fba5...16140ae1a102900babc80a33c44059580f687047)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <[email protected]>
2025-10-20 13:55:49 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
fbfcef1573 Update dependency @lezer/highlight to v1.2.2 (#27573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 13:54:13 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4eecd37aaf Update dependency marked to v16.4.1 (#27574)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 10:52:32 +00:00
TheJulianJESandGitHub c798521ab8 Hide "add hardware" button for hardware integrations (#27572) 2025-10-20 10:52:06 +00:00
Petar PetrovandGitHub e432f0a8ee Fix date test to work on Oct 20 (#27575)
* Fix date test to work on Oct 20

* tweak
2025-10-20 10:39:04 +00:00
Tobias BieniekandGitHub e3a1d0abe2 data/floor_registry: Use 9999 fallback for null floor levels (#27559)
Change the fallback for null floor levels from 0 to 9999, ensuring floors
without a defined level appear at the bottom when sorted (after all numbered
floors, including negative basement levels).

This matches the original intent from #20206 which added support for floors
without levels.
2025-10-20 11:43:59 +03:00
Iván PereiraandGitHub 8080ba696c Add tooltip instead of title for dashboard button (#27563) 2025-10-19 16:23:58 +00:00
7bd8f321a4 Display Zigbee Connection on device page (#27380)
* Update ha-device-info-card.ts

* Update src/panels/config/devices/device-detail/ha-device-info-card.ts

Co-authored-by: Bram Kragten <[email protected]>

* Fix filter syntax in _getAddresses method

* Fix formatting issue in _getAddresses method

* Remove IEEE address from Zigbee info panel

Remove IEEE address display from Zigbee info panel.

---------

Co-authored-by: Bram Kragten <[email protected]>
2025-10-19 16:20:01 +02:00
Niklas WagnerandGitHub 4e958302b4 Allow selecting multiple states in state condition (#27453)
* Allow selecting multiple states in state condition

* Make use of the ensureArray function
2025-10-19 12:23:22 +00:00
Paul BotteinandGitHub 8a42d15bde Use entity naming in more cards and badges (#27541)
* Add support for button card, glance card and entities card

* Add tests

* Add support for attribute and button row

* Add support to heading badge

* Undo changes from rows

* Add comment
2025-10-19 14:08:28 +02:00
wrfzandGitHub ef0da0a7ee Add placeholder text for ha-selector-device (#27551)
* Add placeholder text for ha-selector-device

Added for:
- ha-selector-device
- ha-selector-entity
2025-10-19 14:06:04 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
ae053c20b0 Update dependency @rsdoctor/rspack-plugin to v1.3.3 (#27558)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-18 21:31:52 +02:00
Tobias BieniekandGitHub 5f71938d60 Consolidate floor sorting with floorCompare() (#27553)
* data/floor_registry: Fix `floorCompare()` argument type

* data/floor_registry: Add test suite for `floorCompare()` fn

* data/floor_registry: Add level-based sorting to `floorCompare()`

Update `floorCompare()` to include level-based sorting between custom order
and name sorting, matching the pattern used in the `getFloors()` helper.

Sort priority:
1. Custom order (if provided)
2. Floor level (lower levels first, with 0 fallback for null)
3. Floor name (alphabetical)

This makes `floorCompare()` consistent with the floor sorting logic used
throughout the codebase and prepares it for actual use.

* areas-strategy-helper: Use `floorCompare()` in `getFloors()` helper

Replace duplicated floor sorting logic in `getFloors()` with the
centralized `floorCompare()` function, matching the pattern used
for areas with `areaCompare()`.

This eliminates code duplication and ensures consistent floor sorting
across the codebase.

* data/area_floor: Use `floorCompare()` in `getAreasAndFloors()`

Replace duplicated floor sorting logic in `getAreasAndFloors()` with
`floorCompare()`, passing `haFloors` directly since it's already in
the correct format (Record<string, FloorRegistryEntry>).

This ensures consistent floor sorting across the codebase.
2025-10-18 18:16:30 +00:00
Tobias BieniekandGitHub 82ac26b326 Remove redundant sorting in area and floor registry fetching (#27552)
The initial sorting in `fetchAreaRegistry` and `fetchFloorRegistry` serves
no purpose because the data is immediately converted to an object/Record in
`connection-mixin.ts`, which loses any array ordering. All UI components that
need sorted lists call `Object.values()` and sort the data themselves.
2025-10-18 11:39:06 +03:00
Jan-Philipp BeneckeandGitHub 80b92b9813 Improve styling of ZHA manage device dialog (#27556) 2025-10-18 09:26:58 +03:00
904a083f61 Use progress button for config save button in ZHA dashboard (#27547)
* Use progress button for config save button in ZHA dashboard

* Update src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts

Co-authored-by: Petar Petrov <[email protected]>

* Apply suggestion from @jpbede

* Revert

---------

Co-authored-by: Petar Petrov <[email protected]>
2025-10-17 19:27:01 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
d75ee09d55 Update dependency @lokalise/node-api to v15.3.1 (#27555)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 19:21:08 +03:00
Jan-Philipp BeneckeandGitHub a8e0d506b6 Make target area of slider thumb larger (#27550) 2025-10-17 19:12:08 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
01dd731622 Update dependency typescript-eslint to v8.46.1 (#27546)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 08:30:29 +03:00
Paul BotteinandGitHub dc20702d36 Remove title for common controls section for home dashboard (#27545) 2025-10-16 21:45:53 +02:00
Aidan TimsonandGitHub f32ca9be29 Set header bar min height and make sure items are centered (#27542) 2025-10-16 16:14:08 +02:00
Aidan TimsonandGitHub 8c4c4157a8 Remove unnecessary on-surface-default semantic color (#27536) 2025-10-16 16:07:26 +02:00
WendelinandGitHub c8419d4c3d Improve target picker section title (#27539) 2025-10-16 15:37:04 +02:00
Paul BotteinandGitHub 089316b8ae Fix duplicated name in entity name picker and fix missing entity id support (#27538) 2025-10-16 15:47:47 +03:00
WendelinandGitHub 8d03ac5f64 Fix target picker device/floor icon (#27515)
* Fix device icon alginment

* Expand target item group if new target is in there

* Remove sticky header animation

* Fix type attribute

* Fix floor icon

* Reflect collapsed

* fix 0 entities target

* Improve empty search
2025-10-16 14:09:05 +03:00
WendelinandGitHub e0e1f6f920 use popover with trap focus (#27533)
* use popover with focus trap

* update attribute

* Use new WA
2025-10-16 14:03:00 +03:00
Paul BotteinandGitHub d4c98cae3a Update drag icon (#27514) 2025-10-16 11:33:25 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
46d0eb4f44 Update dependency @codemirror/view to v6.38.6 (#27531)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-16 10:35:26 +02:00
karwostsandGitHub 07812f8d84 Support media-source links for view background (#27522) 2025-10-16 08:42:39 +03:00
Paul BotteinandGitHub 96f54d348f Fix entity badge name (#27520) 2025-10-16 08:38:42 +03:00
Paul BotteinandGitHub 6084ab116f Use empty string for no name instead of empty array for name (#27523) 2025-10-16 08:35:38 +03:00
Simon LamonandGitHub 6b7acd8d3b Only show backup ad when cloud is enabled (#27524) 2025-10-16 08:34:23 +03:00
karwostsandGitHub e35b155c66 Delete image selector (#27519) 2025-10-15 13:32:10 +00:00
Paul BotteinandGitHub 437d02c12f Group dashboards by type (#27517)
Group dashboard by type
2025-10-15 16:11:37 +03:00
Paul BotteinandGitHub 9cd74fbff8 Make custom text more discoverable in entity name picker (#27505)
* Make custom text more discoverable in entity name picker

* Fix custom option selection

* Rename label
2025-10-15 10:07:40 +02:00
karwostsandGitHub 33a7aacd83 Use media selector in picture-glance and picture-elements (#27506) 2025-10-15 08:31:19 +03:00
karwostsandGitHub 39546615bb Missing translation on back button (#27510) 2025-10-15 06:09:30 +02:00
J. Nick KostonandGitHub be51cbc944 Add support for next_flow on abort (#27491) 2025-10-14 11:45:11 -10:00
77874aa2d7 New target picker (#27284)
Co-authored-by: Copilot <[email protected]>
2025-10-14 16:16:34 +02:00
4808463d5f Estimate backup size (#27423)
Co-authored-by: Bram Kragten <[email protected]>
2025-10-14 16:13:46 +02:00
5fb3cab247 Add media support to hui-image and picture-entity-card (#27450)
Co-authored-by: Petar Petrov <[email protected]>
2025-10-14 13:06:23 +00:00
d1093b187f Improved Sankey layout (#26787)
Co-authored-by: Copilot <[email protected]>
2025-10-14 13:53:11 +02:00
Petar PetrovGitHubBram Kragtenrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>karwosts
fd7f0d3841 Add pie chart mode to energy devices graph (#27282)
* Add pie chart mode to energy devices graph

* universal transition

* format

* Add hide_compound_stats option to energy-devices-graph-card (#27263)

* Add hide_compound_stats option to energy-devices-graph-card

* Update src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts

Co-authored-by: Bram Kragten <[email protected]>

* format

---------

Co-authored-by: Bram Kragten <[email protected]>

* Save chart type in storage

* show untracked compound energy and total energy

* Update dependency lint-staged to v16.2.3 (#27285)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Update dependency @codemirror/view to v6.38.4 (#27288)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Add a sub-editor to hui-entity-editor (#27157)

* Add a sub-editor to hui-entity-editor

* item styling

* fix compare order

* handle label click in pie chart

* order compare data based on current data

* show untracked energy in tooltip

* Apply suggestions from code review

Co-authored-by: Bram Kragten <[email protected]>

---------

Co-authored-by: Bram Kragten <[email protected]>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: karwosts <[email protected]>
2025-10-14 11:31:11 +00:00
hanwgandGitHub 36aa74e4a5 Add menu item to copy config entry id (#27394) 2025-10-14 10:26:42 +00:00
Paul BotteinandGitHub 938128d1c3 Don't add audio track if webrtc player is muted (#25767) 2025-10-14 10:21:59 +00:00
Paul BotteinandGitHub 2a5d4ac578 Rename security panel to safety panel (#27502) 2025-10-14 12:09:01 +02:00
be63ff7702 Improve ZHA config dashboard styling (#27492)
Co-authored-by: Aidan Timson <[email protected]>
2025-10-14 11:56:19 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
132c68bf20 Update dependency lint-staged to v16.2.4 (#27499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 11:04:28 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
16499bbd6b Update dependency @rsdoctor/rspack-plugin to v1.3.2 (#27498)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 10:49:45 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
c7eddfed8f Update dependency @types/leaflet to v1.9.21 (#27497)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 10:19:30 +03:00
Jan-Philipp BeneckeandGitHub 150842e431 Fix button wrapping in Z-Wave JS config dashboard (#27493) 2025-10-14 06:10:20 +02:00
9eb5360a68 Update add-on auto-update strings to use full phrase (#27484)
Co-authored-by: balloob <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
2025-10-13 16:38:38 +02:00
e9e32c7d91 Migrate restart wait to ha-wa-dialog (#27476)
Co-authored-by: Bram Kragten <[email protected]>
2025-10-13 13:52:44 +00:00
WendelinandGitHub c83d760e82 Enable keyboard resizing of automation sidebar (#27473) 2025-10-13 15:50:27 +02:00
Paul BotteinandGitHub 489b7f9227 Revert "Add plus and minus button for media player more info" (#27409) 2025-10-13 15:43:44 +02:00
Paul BotteinandGitHub ad2ba63155 Use entity naming in cards and badges (#27428) 2025-10-13 15:43:17 +02:00
Petar PetrovandGitHub 29bc894dbd Improve sampling in trend feature and sensor card (#27190) 2025-10-13 15:36:33 +02:00
karwostsandGitHub faf6cb6333 Disconnect streaming <img> when closing media dialog (#27479) 2025-10-13 15:57:02 +03:00
Aidan TimsonandGitHub a2e1e6362b Migrate new backup dialog to ha-wa-dialog (#27430) 2025-10-13 13:13:26 +02:00
Paul BotteinandGitHub 3212ab6f3b Align more info breadcrumb style with entity picker style for context (#27447) 2025-10-13 10:59:04 +03:00
Paul BotteinandGitHub 3d27daad80 Merge favorite and common controls in home dashboard (#27438) 2025-10-13 10:55:23 +03:00
b679f1ce60 Remove trailing whitespace from ZHA pairing doc link (#27468)
* Remove trailing whitespace from doc link

* format

---------

Co-authored-by: Petar Petrov <[email protected]>
2025-10-13 06:47:03 +00:00
Paul BotteinandGitHub 6b0a5d783b Group area by floor in home dashboard (#27443) 2025-10-13 09:25:25 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
23e2f94d11 Bump softprops/action-gh-release from 2.3.4 to 2.4.1 (#27471)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.3.4 to 2.4.1.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/62c96d0c4e8a889135c1f3a25910db8dbe0e85f7...6da8fa9354ddfdc4aeace5fc48d7f679b5214090)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 2.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 09:22:40 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c250777858 Bump github/codeql-action from 3.30.6 to 4.30.8 (#27472)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.30.6 to 4.30.8.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/64d10c13136e1c5bce3e5fbde8d4906eeaafc885...f443b600d91635bebf5b0d9ebc620189c0d6fba5)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 09:18:57 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
c35d0da9bd Update dependency ua-parser-js to v2.0.6 (#27470)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 09:16:58 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
794aa45a2b Update formatjs monorepo (#27466)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 08:28:33 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
d0b85d0c0b Update dependency core-js to v3.46.0 (#27467)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 08:27:56 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
23b6a3a1a9 Update dependency @bundle-stats/plugin-webpack-filter to v4.21.5 (#27460)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-12 10:22:58 +02:00
Paul BotteinandGitHub 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 BotteinandGitHub 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 BotteinandGitHub 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
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
43a23e6cdd Update dependency marked to v16.4.0 (#27436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 17:55:08 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
aa4dd1cf29 Update dependency @codemirror/view to v6.38.5 (#27445)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 17:48:25 +02:00
Paul BotteinandGitHub 0ae55c39cc Use ha-icon-button for media player more info (#27449)
Use ha-icon-buttom for media player more info
2025-10-10 17:19:48 +02:00
Aidan TimsonandGitHub 0bfacacc9e Update hide sections header helper translation (#27421) 2025-10-10 17:12:38 +02:00
WendelinandGitHub c2f21c19af Fix resizable-bottom-sheet background (#27439) 2025-10-10 14:27:53 +02: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 BotteinandBram Kragten 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 BotteinandBram Kragten 84c4396c13 Add tooltip instead of title for 'add' button (#27399) 2025-10-10 11:35:44 +02:00
karwostsandGitHub 6653333c38 Add media selector to picture-card-editor (#26317) 2025-10-10 11:26:49 +02:00
Aidan TimsonandGitHub 8c19e080be Migrate generate backup dialog to ha-wa-dialog (#27431) 2025-10-10 11:05:53 +02:00
WendelinandGitHub c649b1015a Fix notification badge radius (#27441) 2025-10-10 11:02:53 +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
WendelinandBram Kragten d94fa03411 Fix ha-button keyboard focus (#27437) 2025-10-10 10:38:08 +02:00
Petar PetrovandBram Kragten 0a7007ef9e Escape device names in energy dashboard (#27425) 2025-10-10 10:31:09 +02:00
Paul BotteinandBram Kragten dd12136dee Use right variable for content color in tooltip (#27400) 2025-10-10 10:31:08 +02:00
WendelinandBram Kragten 6e2f89fe3d Fix android tap highlight border radius (#27382) 2025-10-10 10:31:07 +02:00
Jan-Philipp BeneckeandBram Kragten 092085b9af Fix media player more info title calculations (#27360) 2025-10-10 10:31:06 +02:00
Paulus SchoutsenandBram Kragten 1c06eb8661 Add ESPHome to discovery sources (#27327) 2025-10-10 10:31:05 +02:00
Jan-Philipp BeneckeandBram Kragten c7e87b06b5 Fix formatting of position slider tooltip in media player more info (#27326) 2025-10-10 10:31:04 +02:00
Jan-Philipp BeneckeandBram Kragten 38c738c199 Add "media_stop" action to media player controls in more info (#27325) 2025-10-10 10:31:03 +02:00
WendelinandBram Kragten 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 BeneckeandBram Kragten c9feb0b75f Add color tokens for slider thumb and indicator (#27295) 2025-10-10 10:31:00 +02:00
Jan-Philipp BeneckeandBram Kragten 10718c35d1 Make ha-slider not depend on font sizes (#27294) 2025-10-10 10:30:59 +02:00
WendelinandBram Kragten 4dc6a37bad Fix automation editor safe area (#27292) 2025-10-10 10:30:59 +02:00
Jan-Philipp BeneckeandBram Kragten 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 BotteinandBram Kragten e4f008800b Align bottom sheet border radius with resizable bottom sheet (#27280) 2025-10-10 10:30:56 +02:00
Petar PetrovandGitHub 1b6c33efd4 Escape device names in energy dashboard (#27425) 2025-10-10 10:25:21 +02:00
WendelinandGitHub 5cfc34b020 Fix ha-button keyboard focus (#27437) 2025-10-10 10:15:30 +02:00
Petar PetrovandGitHub 1e7647b214 Add unit_class to "recorder/update_statistics_metadata" (#27422)
* Add unit_class to "recorder/update_statistics_metadata"

* update type
2025-10-10 10:51:03 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
cef3a7ef99 Migrate renovate config (#27426)
Migrate config renovate.json

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 20:26:31 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
14d0028426 Update dependency typescript-eslint to v8.46.0 (#27434)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 20:24:44 +02:00
Aidan TimsonandGitHub 28032d9d0d Fix spinner position in move data disk dialog (#27429) 2025-10-09 15:02:52 +01:00
Paul BotteinandGitHub 6c1995ba1b Use dedicated component for sub element using form (#27424) 2025-10-09 15:44:18 +02:00
Aidan TimsonandGitHub b68464c5d5 Fix ha-dialog-header height (#27427) 2025-10-09 14:19:22 +01:00
31ccf114a6 Ability to hide section headers from todo card (#26949)
Co-authored-by: Simon Lamon <[email protected]>
2025-10-09 08:11:03 +01:00
Aidan TimsonandGitHub 1b932ae4a2 Setup webawesome dialog and update standard dialog header design (#27020) 2025-10-09 08:16:25 +02:00
Krzysztof DąbrowskiandGitHub 0df6019b95 Support custom color configuration in button card (#27029)
* Support custom color configuration in button card

* Fix lint issue

* Fix logic for light domain

* Implement state_color migration
2025-10-09 08:54:01 +03:00
TheJulianJESandGitHub 94fb03d2e2 Replace "radio" with "adapter" for Zigbee and Thread (#27414) 2025-10-08 17:40:08 +02:00
Paul BotteinandGitHub 6dc165ebf8 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-08 17:39:15 +02:00
Paul BotteinandGitHub f2c5b91def Revert "Add media playback badge for Area card (#26893)" (#27413)
This reverts commit 7c7a4e61f2.
2025-10-08 15:59:37 +02:00
b312cca050 Show weekday in weather more-info hourly and twice daily forecast (#27402)
Co-authored-by: karwosts <[email protected]>
2025-10-08 15:32:12 +02:00
Norbert RittelandGitHub ac14733bff Change "No device associated" to "No related device" (#27412) 2025-10-08 15:05:41 +02:00
WendelinandGitHub a2d4165511 Improved data-table search (#27396) 2025-10-08 11:03:02 +02:00
Paul BotteinandGitHub b87ffbd4f7 Add name preset to tile card (#27065) 2025-10-08 08:13:54 +00:00
Paul BotteinandGitHub a8f8d197f8 Add tooltip instead of title for 'add' button (#27399) 2025-10-07 17:48:49 +02:00
Paul BotteinandGitHub 4fcac79047 Use right variable for content color in tooltip (#27400) 2025-10-07 15:46:40 +00:00
42ddacd41a Add plus and minus button for media player more info (#27398)
* Add plus and minus button for media player even if it support volume slider

* Update src/dialogs/more-info/controls/more-info-media_player.ts

Co-authored-by: Jan-Philipp Benecke <[email protected]>

* Remove hardcoded support

---------

Co-authored-by: Jan-Philipp Benecke <[email protected]>
2025-10-07 17:43:17 +02:00
ebc9981289 Fix hui-conditional-row causing varying row margins. (#26355)
* Fix hui-conditional-row causing varying row margins.

* Update row gap CSS var

* Apply suggestion from @bramkragten

Co-authored-by: Bram Kragten <[email protected]>

* Apply suggestion from @bramkragten

Co-authored-by: Bram Kragten <[email protected]>

* Apply suggestion from @bramkragten

Co-authored-by: Bram Kragten <[email protected]>

* Use row-visibility-change method fired in hui-conditional-row

* Update to pass row in row-visibility-changed event

---------

Co-authored-by: Bram Kragten <[email protected]>
2025-10-07 16:11:55 +03:00
WendelinandGitHub 23deab253b Add ellipsis to ha-button label (#27391) 2025-10-07 16:03:54 +03:00
Jan-Philipp BeneckeandGitHub ab172abe02 Refactor overflow menu in backups data table to have a single instance (#27383)
* Refactor overflow menu in backups data table to have a single instance

* Fix
2025-10-07 08:39:11 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
10d5d8b15d Update dependency eslint to v9.37.0 (#27390)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 06:58:32 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
c9e472dab7 Update formatjs monorepo (#27389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 06:58:18 +02:00
WendelinandGitHub 1e13b2b812 Fix android tap highlight border radius (#27382) 2025-10-06 20:32:42 +02:00
Jan-Philipp BeneckeandGitHub e04a04632a Replace confirm dialogs with toast for delete actions in automation/script editor (#27324)
* Replace confirm dialogs with toast for delete actions in automation/script editor

* Migrate confirm dialog to toast in option row
2025-10-06 20:21:54 +02:00
Jan-Philipp BeneckeandGitHub 04bc5fba63 Refactor undo/redo to be a controller instead (#27279) 2025-10-06 16:04:42 +02:00
Aidan TimsonandGitHub e66724ca9e Move duplicate css to shared styles for state control toggles (#27377) 2025-10-06 15:48:14 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
bcfe5add33 Update vaadinWebComponents monorepo to v24.9.2 (#27374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 12:56:30 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7cc116dd07 Bump softprops/action-gh-release from 2.3.3 to 2.3.4 (#27366)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.3.3 to 2.3.4.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/6cbd405e2c4e67a21c47fa9e383d020e4e28b836...62c96d0c4e8a889135c1f3a25910db8dbe0e85f7)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 2.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-06 11:18:59 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ee93f31220 Bump actions/stale from 10.0.0 to 10.1.0 (#27365)
Bumps [actions/stale](https://github.com/actions/stale) from 10.0.0 to 10.1.0.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/3a9db7e6a41a89f618792c92c0e97cc736e1b13f...5f858e3efba33a5ca4407a664cc011ad407f2008)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-06 11:18:30 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b7cc19f12e Bump github/codeql-action from 3.30.5 to 3.30.6 (#27364)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.30.5 to 3.30.6.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/3599b3baa15b485a2e49ef411a7a4bb2452e7f93...64d10c13136e1c5bce3e5fbde8d4906eeaafc885)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-06 11:18:00 +03:00
Leslie FernandoandGitHub f70edf9311 Refactor: Replace string concatenation with template literals (#27368) 2025-10-06 09:55:01 +02:00
Leslie FernandoandGitHub 0fa7c2face Refactor: Replace Object.keys().includes() with 'in' operator (#27369) 2025-10-06 09:54:13 +02:00
Leslie FernandoandGitHub 7b3a265a70 Refactor: Replace deprecated substr() with slice() (#27370) 2025-10-06 09:51:51 +02:00
Leslie FernandoandGitHub 5d9aae3ad5 Fix/assist debug interface typo (#27339) 2025-10-06 08:50:53 +02:00
Leslie FernandoandGitHub 5de84ac0d8 Refactor: Simplify boolean return in isSeparatorAtPos (#27362)
Simplifies the default case in isSeparatorAtPos function by directly returning the boolean result of isEmojiImprecise(code) instead of using an if-else statement.

This improves code readability and reduces unnecessary conditional logic while maintaining the same behavior.

Changes:
- Removed verbose if-else pattern
- Direct boolean return
- Reduced cyclomatic complexity
2025-10-06 08:56:53 +03:00
Jan-Philipp BeneckeandGitHub 98c4ec91d6 Refactor overflow menu in labels data table to have a single instance (#27249)
* Refactor overflow menu in labels data table to have a single instance

* Process code review

* Revert
2025-10-06 08:55:37 +03:00
karwostsandGitHub 972b9cb758 Give a warning body in preview for empty stack cards (#27350) 2025-10-06 08:45:20 +03:00
Leslie FernandoandGitHub ac621af811 Fix typo in class name: HaConfigLovelaceRescources HaConfigLovelaceR… (#27358)
Fix typo in class name: HaConfigLovelaceRescources  HaConfigLovelaceResources

Corrects misspelled class name from 'Rescources' to 'Resources' in the Lovelace resources configuration panel.

The filename was correctly spelled (ha-config-lovelace-resources.ts), but the exported class name had the typo.

Changes:
- Fixed export class declaration
- Fixed HTMLElementTagNameMap interface declaration

This improves code consistency and TypeScript type safety.
2025-10-06 08:25:21 +03:00
Leslie FernandoandGitHub 7eb97bb58f Fix typo in parameter name: entites entities in MockHomeAssistant in… (#27357)
Fix typo in parameter name: entites  entities in MockHomeAssistant interface

Corrects misspelled parameter name in the addEntities method signature. The correct spelling is 'entities', not 'entites'.

This improves code consistency and maintains proper TypeScript interface definitions in the test/demo utilities.
2025-10-06 08:24:35 +03:00
karwostsandGitHub d37af0f488 Update Mauritania currency (#27361) 2025-10-06 08:15:19 +03:00
Jan-Philipp BeneckeandGitHub 0d3b340228 Fix media player more info title calculations (#27360) 2025-10-05 22:26:20 +02:00
Leslie FernandoandGitHub 288e03775b Fix typo in variable name: enityA entityA in script config (#27356) 2025-10-05 21:45:23 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
df36e9d205 Update dependency @home-assistant/webawesome to v3.0.0-beta.6.ha.1 (#27349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-05 18:18:55 +02:00
karwostsandGitHub 15a0b35866 Enable partial energy-sources-table by type (#27346)
* Enable partial energy sources table by category

* typing
2025-10-05 18:18:30 +02:00
Leslie FernandoandGitHub aa7522f681 Fix inconsistent variable naming: lokalize localize (#27340)
- Rename variable from 'lokalize' to 'localize' for consistency
- Affects developer-tools action panel and connection mixin
- Matches standard naming convention used throughout codebase
- Improves code readability and maintains naming standards
2025-10-05 11:49:14 +00:00
Leslie FernandoandGitHub c09e97a561 Improve type safety: Replace 'any[]' with generic type parameter in a… (#27334)
Improve type safety: Replace 'any[]' with generic type parameter in arrayFilter function

- Convert arrayFilter from using 'any[]' to generic type <T>
- Improves TypeScript type safety and inference
- Follows strict TypeScript guidelines in codebase
- No behavioral changes, purely type improvement
2025-10-05 14:44:34 +03:00
karwostsandGitHub 733be8e5a3 Fix activity panel date picker clipping (#27341) 2025-10-05 14:43:24 +03:00
Leslie FernandoandGitHub d107ac7d4c Fix comment typo: Change 'remplace' to 'replace' in developer-tools (#27331) 2025-10-05 10:32:26 +00:00
Leslie FernandoandGitHub efc5bacb97 Fix grammar: Change 'can not' to 'cannot' in English translations (#27333) 2025-10-05 10:29:48 +00:00
Leslie FernandoandGitHub 430e52efe3 Fix ARIA role typo: Change 'seperator' to 'separator' in todo-list card (#27335)
- Fixed 4 instances of misspelled ARIA role attribute
- Improves accessibility for screen readers
- Aligns with WCAG guidelines and ARIA specification
2025-10-05 10:29:26 +00:00
Leslie FernandoandGitHub 6b4c4a9cf8 Fix comment typo: Change 'TODo' to 'TODO' in intl-polyfill (#27332)
Correct inconsistent capitalization in TODO comment for better
code consistency and readability.
2025-10-05 10:28:32 +00:00
Leslie FernandoandGitHub e5b1acc2c3 Fix grammar: Add apostrophe to 'don't' in automation trigger comment (#27337) 2025-10-05 10:26:16 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
c89f476d67 Update dependency @codemirror/commands to v6.9.0 (#27348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-05 12:15:46 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
e68afead17 Update dependency @lokalise/node-api to v15.3.0 (#27345)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-05 08:28:11 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
c4651c0bc0 Update dependency eslint-plugin-wc to v3.0.2 (#27343)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-05 08:27:44 +02:00
Paul BotteinandGitHub 6d95b7af11 Move home subview to dedicated dashboard (#27264)
* Create lights panel

* Move strategy

* Move files

* Add security and climate panel

* Continue climate and security migration

* Add settings for climate panel

* Don't show these panel in the sidebar

* Rename lights to light

* Remove climate config for now

* Fix light

* Remove unused import
2025-10-04 17:44:34 -04:00
Jan-Philipp BeneckeandGitHub 3e74cf3ada Fix formatting of position slider tooltip in media player more info (#27326) 2025-10-04 09:13:28 +02:00
Jan-Philipp BeneckeandGitHub 859ee98abb Add color tokens for slider thumb and indicator (#27295) 2025-10-04 07:12:35 +00:00
Jan-Philipp BeneckeandGitHub dd3e5e3724 Add "media_stop" action to media player controls in more info (#27325) 2025-10-04 09:00:08 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2e3ab4d64f Update dependency @codemirror/legacy-modes to v6.5.2 (#27323)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-04 08:59:05 +02:00
Paulus SchoutsenandGitHub 63cbeca820 Add ESPHome to discovery sources (#27327) 2025-10-04 08:58:34 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
1057ff314c Update dependency @bundle-stats/plugin-webpack-filter to v4.21.4 (#27328)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-04 08:52:41 +02:00
renovate[bot]andGitHub 5b946f1048 Update dependency typescript to v5.9.3 (#27329) 2025-10-04 07:36:49 +02:00
fdd66b5cec Use border radius design tokens in codebase (#27169)
Co-authored-by: Simon Lamon <[email protected]>
2025-10-03 11:11:30 +00:00
76c9723c71 Show the total value in energy graphs (#27265)
* Show the total value in energy graphs

* format

* Apply suggestions from code review

Co-authored-by: Bram Kragten <[email protected]>

---------

Co-authored-by: Bram Kragten <[email protected]>
2025-10-03 13:01:29 +02:00
Jan-Philipp BeneckeandGitHub b02368b9c6 Make ha-slider not depend on font sizes (#27294) 2025-10-03 12:49:29 +02:00
WendelinandGitHub 0bcb7897c9 Fix mobile ha-dialog height in Browser (#27298)
Enhance dialog responsiveness by adjusting min/max height to use svh units
2025-10-03 12:48:08 +02:00
Norbert RittelandGitHub 786bbb3850 Capitalize two occurrences of "YAML" abbreviation (#27314) 2025-10-03 12:45:29 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
e8ead84fe5 Update dependency barcode-detector to v3.0.6 (#27318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-03 12:45:06 +02:00
karwostsandGitHub 428e7fb332 Add a placeholder hint for template selector (#27297)
Add a placeholder for template selector
2025-10-03 08:38:58 +03:00
karwostsandGitHub ad9e8d5a52 Add entity sub-editor to picture glance (#27312) 2025-10-03 08:32:39 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
e3cf04b3d1 Update octokit monorepo to v8.0.2 (#27308)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-03 06:46:49 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
10c3042db1 Update dependency typescript-eslint to v8.45.0 (#27309)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-03 06:46:25 +02:00
25f6b7de2f Add energy compare with previous year (#25037)
* Add energy compare with previous year

* minor text adjustment

---------

Co-authored-by: Petar Petrov <[email protected]>
2025-10-02 19:08:57 +03:00
Aidan TimsonandGitHub ca1cda4824 Fix desktop translation for browser media source (#27293)
Fix translation for desktop
2025-10-02 16:36:12 +03:00
WendelinandGitHub 8c4a67315b Fix automation editor safe area (#27292) 2025-10-02 12:01:47 +02:00
Paul BotteinandGitHub c18de97b32 Align bottom sheet border radius with resizable bottom sheet (#27280) 2025-10-02 11:44:54 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
23a3ca3ed7 Update dependency @rspack/core to v1.5.8 (#27291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 12:32:44 +03:00
Jan-Philipp BeneckeandGitHub 69457b4e85 Support redo on Shift+CMD+Z (#27287)
* Support redo on Shift+CMD+Z

* Update redo shortcut for macOS to CMD+Shift+Z
2025-10-02 11:56:56 +03:00
WendelinandGitHub 2e096c23e0 Remove @shoelace-style from babelOptions exclusion list (#27289) 2025-10-02 10:33:23 +02:00
karwostsandGitHub 552691e200 Add a sub-editor to hui-entity-editor (#27157)
* Add a sub-editor to hui-entity-editor

* item styling
2025-10-02 08:19:24 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
91258c86c1 Update dependency @codemirror/view to v6.38.4 (#27288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 08:06:29 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3750a378cd Update dependency lint-staged to v16.2.3 (#27285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 19:29:17 +02:00
12d3304c72 Add hide_compound_stats option to energy-devices-graph-card (#27263)
* Add hide_compound_stats option to energy-devices-graph-card

* Update src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts

Co-authored-by: Bram Kragten <[email protected]>

* format

---------

Co-authored-by: Bram Kragten <[email protected]>
2025-10-01 16:24:10 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
246100809d Update dependency lint-staged to v16.2.2 (#27276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 12:38:43 +02:00
Michael HergerandGitHub 6efca93186 Introduce new number formatting for Switzerland (#27268) 2025-10-01 11:08:25 +02:00
Simon LamonandGitHub 6280647b9a ha-refresh-tokens-card: Replace ha-button-menu to ha-md-button-menu (#26874) 2025-10-01 10:45:34 +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
WendelinandBram Kragten 1161de5746 Update WA to fix tab group scrolling (#27255) 2025-10-01 08:51:12 +02:00
Jan-Philipp BeneckeandBram Kragten 9df8e20391 Use local entity picture if available in media player more info (#27252) 2025-10-01 08:51:11 +02:00
Petar PetrovandBram Kragten 11047a9c95 Make "loading next step" look like progress step in config flows (#27234) 2025-10-01 08:51:10 +02:00
Jan-Philipp BeneckeandBram Kragten 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 LamonandBram Kragten 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 BeneckeandBram Kragten ee0fc360b0 Add custom color token for control color (#27227) 2025-10-01 08:51:07 +02:00
Jan-Philipp BeneckeandBram Kragten 4012f95ec1 Add tooltips for undo/redo in automation & script editors (#27224) 2025-10-01 08:51:06 +02:00
karwostsandGitHub 2ff52c6c29 Add alert to fixed domain states (#27271) 2025-10-01 09:07:24 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
d038e11170 Update dependency @rsdoctor/rspack-plugin to v1.3.1 (#27273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 09:03:09 +03:00
karwostsandGitHub 8925b39fe5 Make enum colors stable in history chart (#27272) 2025-10-01 08:52:42 +03:00
karwostsandGitHub beeef65506 State colors for weather (#27270)
* State colors for weather

* Update color.globals.ts

minor white tuning

* Update color.globals.ts

cloudy color change
2025-10-01 08:48:37 +03:00
Bram KragtenandGitHub 994c1b5751 Fix intl polyfill loading (#27261) 2025-09-30 16:47:12 +03:00
Aidan TimsonandGitHub 6823c647b6 Fix calendar card height (#27052) 2025-09-30 15:27:46 +02:00
Jan-Philipp BeneckeandGitHub 866b478dc0 Use local entity picture if available in media player more info (#27252) 2025-09-30 14:36:39 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
d746dc5752 Pin Node.js to 22.20.0 (#27258)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 11:53:51 +02:00
WendelinandGitHub 5f53e1e71c Update WA to fix tab group scrolling (#27255) 2025-09-30 11:36:17 +02:00
WendelinandGitHub 3da82df093 Update node nvm to latest LTS (#27256) 2025-09-30 11:35:48 +02:00
4cedfffb71 Let text scroll in markdown card (#27250)
* Let text overflow in markdown card

* Update src/panels/lovelace/cards/hui-markdown-card.ts

Co-authored-by: Paul Bottein <[email protected]>

---------

Co-authored-by: Paul Bottein <[email protected]>
2025-09-30 09:37:15 +02:00
dcapslockandGitHub 1e1514e7da forwardHaptic on node rather than window. (#27251)
forwardHaptic on node rather than window. Allows for capturing for custom cards.
2025-09-30 07:49:32 +03:00
Jan-Philipp BeneckeandGitHub 60e07075bc Refactor ha-config-labels to use styleMap (#27248) 2025-09-29 21:05:17 +02:00
Jan-Philipp BeneckeandGitHub c998086474 Fix --ha-space-13 spacing token (#27246) 2025-09-29 20:16:29 +02:00
Paul BotteinandGitHub 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 BeneckeandPaul Bottein 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
karwostsandPaul Bottein 6ae24b8135 Add validation issues to energy diagnostic (#27203) 2025-09-26 15:38:18 +02:00
karwostsandPaul Bottein 8e009f24f9 Add dropdown mode to water heater operation feature (#27201) 2025-09-26 15:38:17 +02:00
Simon LamonandPaul Bottein 53031f44ac Fix typos in media player more info (#27198) 2025-09-26 15:38:16 +02:00
Jan-Philipp BeneckeandPaul Bottein af5a988457 Round seconds in media player more info before formatting (#27196) 2025-09-26 15:38:15 +02:00
Paul BotteinandGitHub 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
WendelinandPaul Bottein 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 BotteinandGitHub 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 RittelandPaul Bottein 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 SchoutsenandPaul Bottein 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 BotteinandGitHub 7a285f11db 20250924.0 (#27155) 2025-09-24 17:15:37 +02:00
633 changed files with 22048 additions and 8676 deletions
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -61,7 +61,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
+6 -6
View File
@@ -26,7 +26,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -60,7 +60,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -78,7 +78,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -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
@@ -102,7 +102,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -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
+3 -3
View File
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -62,7 +62,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
+3 -3
View File
@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -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
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@1707825cbfcc7452b2913d273414705415ae64d4 # v3.0.1
uses: relative-ci/agent-action@8504826a02078b05756e4c07e380023cc2c4274a # v3.1.0
with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}
+7 -7
View File
@@ -34,7 +34,7 @@ jobs:
uses: home-assistant/actions/helpers/verify-version@master
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -55,7 +55,7 @@ jobs:
script/release
- name: Upload release assets
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
with:
files: |
dist/*.whl
@@ -75,7 +75,7 @@ jobs:
# home-assistant/wheels doesn't support SHA pinning
- name: Build wheels
uses: home-assistant/wheels@2025.09.1
uses: home-assistant/wheels@2025.10.0
with:
abi: cp313
tag: musllinux_1_2
@@ -93,7 +93,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -108,7 +108,7 @@ jobs:
- name: Tar folder
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
- name: Upload release asset
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
@@ -122,7 +122,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -137,6 +137,6 @@ jobs:
- name: Tar folder
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
- name: Upload release asset
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90
+1
View File
@@ -5,6 +5,7 @@
build/
dist/
/hass_frontend/
/logs/dist/
/translations/
# yarn
+1 -1
View File
@@ -1 +1 @@
lts/iron
22.21.1
+18 -4
View File
@@ -18,16 +18,16 @@ module.exports.sourceMapURL = () => {
module.exports.ignorePackages = () => [];
// Files from NPM packages that we should replace with empty file
module.exports.emptyPackages = ({ isHassioBuild }) =>
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
[
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
// Icons in supervisor conflict with icons in HA so we don't load.
isHassioBuild &&
(isHassioBuild || isLandingPageBuild) &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
),
isHassioBuild &&
(isHassioBuild || isLandingPageBuild) &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
),
@@ -183,7 +183,6 @@ module.exports.babelOptions = ({
include: /\/node_modules\//,
exclude: [
"element-internals-polyfill",
"@shoelace-style",
"@?lit(?:-labs|-element|-html)?",
].map((p) => new RegExp(`/node_modules/${p}/`)),
},
@@ -328,6 +327,20 @@ module.exports.config = {
};
},
logs({ isProdBuild, latestBuild, isStatsBuild }) {
return {
name: "logs" + nameSuffix(latestBuild),
entry: {
entrypoint: path.resolve(paths.logs_dir, "src/entrypoint.ts"),
},
outputPath: outputPath(paths.logs_output_root, latestBuild),
publicPath: publicPath(latestBuild),
isProdBuild,
latestBuild,
isStatsBuild,
};
},
landingPage({ isProdBuild, latestBuild }) {
return {
name: "landing-page" + nameSuffix(latestBuild),
@@ -338,6 +351,7 @@ module.exports.config = {
publicPath: publicPath(latestBuild),
isProdBuild,
latestBuild,
isLandingPageBuild: true,
};
},
};
+7
View File
@@ -39,6 +39,13 @@ gulp.task(
)
);
gulp.task(
"clean-logs",
gulp.parallel("clean-translations", async () =>
deleteSync([paths.logs_output_root, paths.build_dir])
)
);
gulp.task(
"clean-landing-page",
gulp.parallel("clean-translations", async () =>
+18
View File
@@ -245,6 +245,24 @@ gulp.task(
)
);
const LOGS_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
gulp.task(
"gen-pages-logs-dev",
genPagesDevTask(LOGS_PAGE_ENTRIES, paths.logs_dir, paths.logs_output_root)
);
gulp.task(
"gen-pages-logs-prod",
genPagesProdTask(
LOGS_PAGE_ENTRIES,
paths.logs_dir,
paths.logs_output_root,
paths.logs_output_latest,
paths.logs_output_es5
)
);
const LANDING_PAGE_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
gulp.task(
+10
View File
@@ -202,6 +202,16 @@ gulp.task("copy-static-gallery", async () => {
copyMdiIcons(paths.gallery_output_static);
});
gulp.task("copy-static-logs", async () => {
// Copy app static files
fs.copySync(polyPath("public/static"), paths.logs_output_static);
copyFonts(paths.logs_output_static);
copyTranslations(paths.logs_output_static);
copyLocaleData(paths.logs_output_static);
copyMdiIcons(paths.logs_output_static);
});
gulp.task("copy-static-landing-page", async () => {
// Copy landing-page static files
fs.copySync(
+1
View File
@@ -7,6 +7,7 @@ import "./download-translations.js";
import "./entry-html.js";
import "./fetch-nightly-translations.js";
import "./gallery.js";
import "./logs.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./hassio.js";
+39
View File
@@ -0,0 +1,39 @@
import gulp from "gulp";
import "./clean.js";
import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./translations.js";
import "./rspack.js";
gulp.task(
"develop-logs",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean-logs",
gulp.parallel(
"gen-icons-json",
"gen-pages-logs-dev",
"build-translations",
"build-locale-data"
),
"copy-static-logs",
"rspack-dev-server-logs"
)
);
gulp.task(
"build-logs",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-logs",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-logs",
"rspack-prod-logs",
"gen-pages-logs-prod"
)
);
+20
View File
@@ -15,6 +15,7 @@ import {
createGalleryConfig,
createHassioConfig,
createLandingPageConfig,
createLogsConfig,
} from "../rspack.cjs";
const bothBuilds = (createConfigFunc, params) => [
@@ -204,6 +205,25 @@ gulp.task("rspack-prod-gallery", () =>
)
);
gulp.task("rspack-dev-server-logs", () =>
runDevServer({
compiler: rspack(
createLogsConfig({ isProdBuild: false, latestBuild: true })
),
contentBase: paths.logs_output_root,
port: 5647,
})
);
gulp.task("rspack-prod-logs", () =>
prodBuild(
bothBuilds(createLogsConfig, {
isProdBuild: true,
isStatsBuild: env.isStatsBuild(),
})
)
);
gulp.task("rspack-watch-landing-page", () => {
// This command will run forever because we don't close compiler
rspack(
+6
View File
@@ -59,5 +59,11 @@ module.exports = {
hassio_output_es5: path.resolve(__dirname, "../hassio/build/frontend_es5"),
hassio_publicPath: "/api/hassio/app",
logs_dir: path.resolve(__dirname, "../logs"),
logs_output_root: path.resolve(__dirname, "../logs/dist"),
logs_output_static: path.resolve(__dirname, "../logs/dist/static"),
logs_output_latest: path.resolve(__dirname, "../logs/dist/frontend_latest"),
logs_output_es5: path.resolve(__dirname, "../logs/dist/frontend_es5"),
translations_src: path.resolve(__dirname, "../src/translations"),
};
+10 -1
View File
@@ -41,6 +41,7 @@ const createRspackConfig = ({
isStatsBuild,
isTestBuild,
isHassioBuild,
isLandingPageBuild,
dontHash,
}) => {
if (!dontHash) {
@@ -168,7 +169,9 @@ const createRspackConfig = ({
},
}),
new rspack.NormalModuleReplacementPlugin(
new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")),
new RegExp(
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).join("|")
),
path.resolve(paths.root_dir, "src/util/empty.js")
),
!isProdBuild && new LogStartCompilePlugin(),
@@ -299,6 +302,11 @@ const createHassioConfig = ({
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
const createLogsConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
createRspackConfig(
bundle.config.logs({ isProdBuild, latestBuild, isStatsBuild })
);
const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
@@ -308,6 +316,7 @@ module.exports = {
createCastConfig,
createHassioConfig,
createGalleryConfig,
createLogsConfig,
createRspackConfig,
createLandingPageConfig,
};
+5 -4
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`
@@ -242,7 +243,7 @@ class HcCast extends LitElement {
}
.question:before {
border-radius: 4px;
border-radius: var(--ha-border-radius-sm);
position: absolute;
top: 0;
right: 0;
+2 -1
View File
@@ -95,7 +95,8 @@ class HcLayout extends LitElement {
}
.hero {
border-radius: 4px 4px 0 0;
border-radius: var(--ha-border-radius-sm) var(--ha-border-radius-sm)
var(--ha-border-radius-square) var(--ha-border-radius-square);
}
.subtitle {
font-size: var(--ha-font-size-m);
+2 -2
View File
@@ -208,7 +208,7 @@ class HaGallery extends LitElement {
}
.sidebar a[active]::before {
border-radius: 12px;
border-radius: var(--ha-border-radius-lg);
position: absolute;
top: 0;
right: 2px;
@@ -241,7 +241,7 @@ class HaGallery extends LitElement {
text-align: center;
margin: 16px;
padding: 16px;
border-radius: 12px;
border-radius: var(--ha-border-radius-lg);
background-color: var(--primary-background-color);
}
@@ -9,10 +9,10 @@ import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import "../../../../src/components/ha-control-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-control-button";
import "../../../../src/components/ha-control-button-group";
import "../../../../src/components/ha-svg-icon";
interface Button {
label: string;
@@ -156,17 +156,17 @@ export class DemoHaBarButton extends LitElement {
--control-button-icon-color: var(--primary-color);
--control-button-background-color: var(--primary-color);
--control-button-background-opacity: 0.2;
--control-button-border-radius: 18px;
--control-button-border-radius: var(--ha-border-radius-xl);
height: 100px;
width: 100px;
}
.custom-group {
--control-button-group-thickness: 100px;
--control-button-group-border-radius: 36px;
--control-button-group-border-radius: var(--ha-border-radius-6xl);
--control-button-group-spacing: 20px;
}
.custom-group ha-control-button {
--control-button-border-radius: 18px;
--control-button-border-radius: var(--ha-border-radius-xl);
--mdc-icon-size: 32px;
}
.vertical-buttons {
@@ -1,10 +1,10 @@
import type { TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-control-number-buttons";
import { repeat } from "lit/directives/repeat";
import { ifDefined } from "lit/directives/if-defined";
const buttons: {
id: string;
@@ -94,7 +94,7 @@ export class DemoHarControlNumberButtons extends LitElement {
--control-number-buttons-background-color: #2196f3;
--control-number-buttons-background-opacity: 0.1;
--control-number-buttons-thickness: 100px;
--control-number-buttons-border-radius: 36px;
--control-number-buttons-border-radius: var(--ha-border-radius-6xl);
}
`;
}
@@ -131,7 +131,7 @@ export class DemoHaControlSelectMenu extends LitElement {
--control-button-icon-color: var(--primary-color);
--control-button-background-color: var(--primary-color);
--control-button-background-opacity: 0.2;
--control-button-border-radius: 18px;
--control-button-border-radius: var(--ha-border-radius-xl);
height: 100px;
width: 100px;
}
@@ -187,7 +187,7 @@ export class DemoHaControlSelect extends LitElement {
--mdc-icon-size: 24px;
--control-select-color: var(--state-fan-active-color);
--control-select-thickness: 130px;
--control-select-border-radius: 36px;
--control-select-border-radius: var(--ha-border-radius-6xl);
}
.vertical-selects {
height: 300px;
@@ -3,8 +3,8 @@ import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import "../../../../src/components/ha-control-slider";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-control-slider";
const sliders: {
id: string;
@@ -151,7 +151,7 @@ export class DemoHaBarSlider extends LitElement {
--control-slider-background: #ffcf4c;
--control-slider-background-opacity: 0.2;
--control-slider-thickness: 130px;
--control-slider-border-radius: 36px;
--control-slider-border-radius: var(--ha-border-radius-6xl);
}
.vertical-sliders {
height: 300px;
@@ -9,8 +9,8 @@ import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import "../../../../src/components/ha-control-switch";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-control-switch";
const switches: {
id: string;
@@ -118,7 +118,7 @@ export class DemoHaControlSwitch extends LitElement {
--control-switch-on-color: var(--green-color);
--control-switch-off-color: var(--red-color);
--control-switch-thickness: 130px;
--control-switch-border-radius: 36px;
--control-switch-border-radius: var(--ha-border-radius-6xl);
--control-switch-padding: 6px;
--mdc-icon-size: 24px;
}
@@ -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.
@@ -131,7 +131,7 @@ export class DemoHaSelectBox extends LitElement {
--mdc-icon-size: 24px;
--control-select-color: var(--state-fan-active-color);
--control-select-thickness: 130px;
--control-select-border-radius: 36px;
--control-select-border-radius: var(--ha-border-radius-6xl);
}
p.title {
@@ -34,3 +34,5 @@ Check the [webawesome documentation](https://webawesome.com/docs/components/slid
**CSS Custom Properties**
- `--ha-slider-track-size` - Height of the slider track. Defaults to `4px`.
- `--ha-slider-thumb-color` - Color of the slider thumb. Defaults to `var(--primary-color)`.
- `--ha-slider-indicator-color` - Color of the filled portion of the slider track. Defaults to `var(--primary-color)`.
+1 -1
View File
@@ -79,7 +79,7 @@ export class DemoHaSlider extends LitElement {
background-color: var(--primary-background-color);
padding: 0 50px;
margin: 16px;
border-radius: 8px;
border-radius: var(--ha-border-radius-md);
}
ha-card {
margin: 24px auto;
+1 -1
View File
@@ -61,7 +61,7 @@ export class DemoHaSpinner extends LitElement {
background-color: var(--primary-background-color);
padding: 0 50px;
margin: 16px;
border-radius: 8px;
border-radius: var(--ha-border-radius-md);
}
ha-card {
margin: 24px auto;
@@ -0,0 +1,3 @@
---
title: Dialog (ha-wa-dialog)
---
@@ -0,0 +1,523 @@
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { mdiCog, mdiHelp } from "@mdi/js";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-dialog-footer";
import "../../../../src/components/ha-form/ha-form";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-wa-dialog";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
const SCHEMA: HaFormSchema[] = [
{ type: "string", name: "Name", default: "", autofocus: true },
{ type: "string", name: "Email", default: "" },
];
type DialogType =
| false
| "basic"
| "basic-subtitle-below"
| "basic-subtitle-above"
| "form"
| "actions";
@customElement("demo-components-ha-wa-dialog")
export class DemoHaWaDialog extends LitElement {
@state() private _openDialog: DialogType = false;
protected render() {
return html`
<div class="content">
<h1>Dialog <code>&lt;ha-wa-dialog&gt;</code></h1>
<p class="subtitle">Dialog component built with WebAwesome.</p>
<h2>Demos</h2>
<div class="buttons">
<ha-button @click=${this._handleOpenDialog("basic")}
>Basic dialog</ha-button
>
<ha-button @click=${this._handleOpenDialog("basic-subtitle-below")}
>Basic dialog with subtitle below</ha-button
>
<ha-button @click=${this._handleOpenDialog("basic-subtitle-above")}
>Basic dialog with subtitle above</ha-button
>
<ha-button @click=${this._handleOpenDialog("form")}
>Dialog with form</ha-button
>
<ha-button @click=${this._handleOpenDialog("actions")}
>Dialog with actions</ha-button
>
</div>
<ha-wa-dialog
.open=${this._openDialog === "basic"}
header-title="Basic dialog"
@closed=${this._handleClosed}
>
<div>Dialog content</div>
</ha-wa-dialog>
<ha-wa-dialog
.open=${this._openDialog === "basic-subtitle-below"}
header-title="Basic dialog with subtitle"
header-subtitle="This is a basic dialog with a subtitle below"
@closed=${this._handleClosed}
>
<div>Dialog content</div>
</ha-wa-dialog>
<ha-wa-dialog
.open=${this._openDialog === "basic-subtitle-above"}
header-title="Dialog with subtitle above"
header-subtitle="This is a basic dialog with a subtitle above"
header-subtitle-position="above"
@closed=${this._handleClosed}
>
<div>Dialog content</div>
</ha-wa-dialog>
<ha-wa-dialog
.open=${this._openDialog === "form"}
header-title="Dialog with form"
header-subtitle="This is a dialog with a form and a footer"
prevent-scrim-close
@closed=${this._handleClosed}
>
<ha-form autofocus .schema=${SCHEMA}></ha-form>
<ha-dialog-footer slot="footer">
<ha-button
data-dialog="close"
slot="secondaryAction"
variant="plain"
>Cancel</ha-button
>
<ha-button data-dialog="close" slot="primaryAction" variant="accent"
>Submit</ha-button
>
</ha-dialog-footer>
</ha-wa-dialog>
<ha-wa-dialog
.open=${this._openDialog === "actions"}
header-title="Dialog with actions"
header-subtitle="This is a dialog with header actions"
@closed=${this._handleClosed}
>
<div slot="headerActionItems">
<ha-icon-button label="Settings" path=${mdiCog}></ha-icon-button>
<ha-icon-button label="Help" path=${mdiHelp}></ha-icon-button>
</div>
<div>Dialog content</div>
</ha-wa-dialog>
<h2>Design</h2>
<h3>Width</h3>
<p>There are multiple widths available for the dialog.</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>small</code></td>
<td><code>min(320px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>medium</code></td>
<td><code>min(580px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>large</code></td>
<td><code>min(720px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>full</code></td>
<td><code>var(--full-width)</code></td>
</tr>
</tbody>
</table>
<p>
<code>--full-width</code> is calculated based on the available width
of the screen. 95vw is the maximum width of the dialog on a large
screen, while on a small screen it is 100vw minus the safe area
insets.
</p>
<p>Dialogs have a default width of <code>medium</code>.</p>
<h3>Prevent scrim close</h3>
<p>
You can prevent the dialog from being closed by clicking the
scrim/overlay. This is allowed by default.
</p>
<h3>Header</h3>
<p>The header contains a title, a subtitle and action items.</p>
<table>
<thead>
<tr>
<th>Slot</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>header</code></td>
<td>The entire header area.</td>
</tr>
<tr>
<td><code>headerTitle</code></td>
<td>The header title text.</td>
</tr>
<tr>
<td><code>headerSubtitle</code></td>
<td>The header subtitle text.</td>
</tr>
<tr>
<td><code>headerActionItems</code></td>
<td>The header action items.</td>
</tr>
</tbody>
</table>
<h4>Header title</h4>
<p>The header title is a text string.</p>
<h4>Header subtitle</h4>
<p>The header subtitle is a text string.</p>
<h4>Header action items</h4>
<p>
The header action items usually containing icon buttons and/or menu
buttons.
</p>
<h3>Body</h3>
<p>The body is the content of the dialog.</p>
<h3>Footer</h3>
<p>The footer is the footer of the dialog.</p>
<p>
It is recommended to use the <code>ha-dialog-footer</code> component
for the footer and to style the buttons inside the footer as so:
</p>
<table>
<thead>
<tr>
<th>Slot</th>
<th>Description</th>
<th>Variant to use</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>secondaryAction</code></td>
<td>The secondary action button(s).</td>
<td><code>plain</code></td>
</tr>
<tr>
<td><code>primaryAction</code></td>
<td>The primary action button(s).</td>
<td><code>accent</code></td>
</tr>
</tbody>
</table>
<h2>Implementation</h2>
<h3>Example Usage</h3>
<pre><code>&lt;ha-wa-dialog
open
header-title="Dialog title"
header-subtitle="Dialog subtitle"
prevent-scrim-close
&gt;
&lt;div slot="headerActionItems"&gt;
&lt;ha-icon-button label="Settings" path="mdiCog"&gt;&lt;/ha-icon-button&gt;
&lt;ha-icon-button label="Help" path="mdiHelp"&gt;&lt;/ha-icon-button&gt;
&lt;/div&gt;
&lt;div&gt;Dialog content&lt;/div&gt;
&lt;ha-dialog-footer slot="footer"&gt;
&lt;ha-button data-dialog="close" slot="secondaryAction" variant="plain"
&gt;Cancel&lt;/ha-button
&gt;
&lt;ha-button slot="primaryAction" variant="accent"&gt;Submit&lt;/ha-button&gt;
&lt;/ha-dialog-footer&gt;
&lt;/ha-wa-dialog&gt;</code></pre>
<h3>API</h3>
<p>
This component is based on the webawesome dialog component. Check the
<a
href="https://webawesome.com/docs/components/dialog/"
target="_blank"
rel="noopener noreferrer"
>webawesome documentation</a
>
for more details.
</p>
<h4>Attributes</h4>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
<th>Options</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>open</code></td>
<td>Controls the dialog open state.</td>
<td><code>false</code></td>
<td><code>false</code>, <code>true</code></td>
</tr>
<tr>
<td><code>width</code></td>
<td>Preferred dialog width preset.</td>
<td><code>medium</code></td>
<td>
<code>small</code>, <code>medium</code>, <code>large</code>,
<code>full</code>
</td>
</tr>
<tr>
<td><code>prevent-scrim-close</code></td>
<td>
Prevents closing the dialog by clicking the scrim/overlay.
</td>
<td><code>false</code></td>
<td><code>true</code></td>
</tr>
<tr>
<td><code>header-title</code></td>
<td>Header title text when no custom title slot is provided.</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>header-subtitle</code></td>
<td>
Header subtitle text when no custom subtitle slot is provided.
</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>header-subtitle-position</code></td>
<td>Position of the subtitle relative to the title.</td>
<td><code>below</code></td>
<td><code>above</code>, <code>below</code></td>
</tr>
<tr>
<td><code>flexcontent</code></td>
<td>
Makes the dialog body a flex container for flexible layouts.
</td>
<td><code>false</code></td>
<td><code>false</code>, <code>true</code></td>
</tr>
</tbody>
</table>
<h4>CSS Custom Properties</h4>
<table>
<thead>
<tr>
<th>CSS Property</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--dialog-content-padding</code></td>
<td>Padding for dialog content sections.</td>
</tr>
<tr>
<td><code>--ha-dialog-show-duration</code></td>
<td>Show animation duration.</td>
</tr>
<tr>
<td><code>--ha-dialog-hide-duration</code></td>
<td>Hide animation duration.</td>
</tr>
<tr>
<td><code>--ha-dialog-surface-background</code></td>
<td>Dialog background color.</td>
</tr>
<tr>
<td><code>--ha-dialog-border-radius</code></td>
<td>Border radius of the dialog surface.</td>
</tr>
<tr>
<td><code>--dialog-z-index</code></td>
<td>Z-index for the dialog.</td>
</tr>
<tr>
<td><code>--dialog-surface-position</code></td>
<td>CSS position of the dialog surface.</td>
</tr>
<tr>
<td><code>--dialog-surface-margin-top</code></td>
<td>Top margin for the dialog surface.</td>
</tr>
</tbody>
</table>
<h4>Events</h4>
<table>
<thead>
<tr>
<th>Event</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>opened</code></td>
<td>Fired when the dialog is shown.</td>
</tr>
<tr>
<td><code>closed</code></td>
<td>Fired after the dialog is hidden.</td>
</tr>
</tbody>
</table>
</div>
`;
}
private _handleOpenDialog = (dialog: DialogType) => () => {
this._openDialog = dialog;
};
private _handleClosed = () => {
this._openDialog = false;
};
static styles = [
css`
:host {
display: block;
padding: var(--ha-space-4);
}
.content {
max-width: 1000px;
margin: 0 auto;
}
h1 {
margin-top: 0;
margin-bottom: var(--ha-space-2);
}
h2 {
margin-top: var(--ha-space-6);
margin-bottom: var(--ha-space-3);
}
h3,
h4 {
margin-top: var(--ha-space-4);
margin-bottom: var(--ha-space-2);
}
p {
margin: var(--ha-space-2) 0;
line-height: 1.6;
}
.subtitle {
color: var(--secondary-text-color);
font-size: 1.1em;
margin-bottom: var(--ha-space-4);
}
table {
width: 100%;
border-collapse: collapse;
margin: var(--ha-space-3) 0;
}
th,
td {
text-align: left;
padding: var(--ha-space-2);
border-bottom: 1px solid var(--divider-color);
}
th {
font-weight: 500;
}
code {
background-color: var(--secondary-background-color);
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
}
pre {
background-color: var(--secondary-background-color);
padding: var(--ha-space-3);
border-radius: 8px;
overflow-x: auto;
margin: var(--ha-space-3) 0;
}
pre code {
background-color: transparent;
padding: 0;
}
.buttons {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: var(--ha-space-2);
margin: var(--ha-space-4) 0;
}
a {
color: var(--primary-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-wa-dialog": DemoHaWaDialog;
}
}
+3 -2
View File
@@ -5,13 +5,13 @@ import type {
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { mockIcons } from "../../../../demo/src/stubs/icons";
import { computeDomain } from "../../../../src/common/entity/compute_domain";
import { computeStateDisplay } from "../../../../src/common/entity/compute_state_display";
import "../../../../src/components/data-table/ha-data-table";
import type { DataTableColumnContainer } from "../../../../src/components/data-table/ha-data-table";
import "../../../../src/components/entity/state-badge";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { mockIcons } from "../../../../demo/src/stubs/icons";
import type { HomeAssistant } from "../../../../src/types";
const SENSOR_DEVICE_CLASSES = [
@@ -39,6 +39,7 @@ const SENSOR_DEVICE_CLASSES = [
"pm1",
"pm10",
"pm25",
"pm4",
"power_factor",
"power",
"precipitation",
@@ -434,7 +435,7 @@ export class DemoEntityState extends LitElement {
display: block;
height: 20px;
width: 20px;
border-radius: 10px;
border-radius: var(--ha-border-radius-md);
background-color: rgb(--color);
}
`;
+1 -1
View File
@@ -121,7 +121,7 @@ class HassioCardContent extends LitElement {
height: 12px;
top: 8px;
right: 8px;
border-radius: 50%;
border-radius: var(--ha-border-radius-circle);
}
.topbar {
position: absolute;
@@ -164,7 +164,7 @@ class HassioHardwareDialog extends LitElement {
pre,
code {
background-color: var(--markdown-code-background-color, none);
border-radius: 3px;
border-radius: var(--ha-border-radius-sm);
}
pre {
padding: 16px;
@@ -228,7 +228,7 @@ class HassioRegistriesDialog extends LitElement {
css`
.registry {
border: 1px solid var(--divider-color);
border-radius: 4px;
border-radius: var(--ha-border-radius-sm);
margin-top: 4px;
}
.action {
@@ -193,7 +193,7 @@ class HassioRepositoriesDialog extends LitElement {
}
.option {
border: 1px solid var(--divider-color);
border-radius: 4px;
border-radius: var(--ha-border-radius-sm);
margin-top: 4px;
}
ha-button {
@@ -302,7 +302,7 @@ class LandingPageLogs extends LitElement {
max-height: 300px;
overflow: auto;
border: 1px solid var(--divider-color);
border-radius: 4px;
border-radius: var(--ha-border-radius-sm);
padding: 4px;
}
+20 -30
View File
@@ -1,22 +1,25 @@
import "@material/mwc-linear-progress";
import { type PropertyValues, css, html, nothing } from "lit";
import { mdiOpenInNew } from "@mdi/js";
import { css, html, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { extractSearchParam } from "../../src/common/url/search-params";
import "../../src/components/ha-alert";
import "../../src/components/ha-button";
import "../../src/components/ha-fade-in";
import "../../src/components/ha-spinner";
import { haStyle } from "../../src/resources/styles";
import "../../src/onboarding/onboarding-welcome-links";
import "./components/landing-page-network";
import "./components/landing-page-logs";
import { extractSearchParam } from "../../src/common/url/search-params";
import { onBoardingStyles } from "../../src/onboarding/styles";
import "../../src/components/ha-svg-icon";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { LandingPageBaseElement } from "./landing-page-base-element";
import "../../src/onboarding/onboarding-welcome-links";
import { onBoardingStyles } from "../../src/onboarding/styles";
import { haStyle } from "../../src/resources/styles";
import "./components/landing-page-logs";
import "./components/landing-page-network";
import {
getSupervisorNetworkInfo,
pingSupervisor,
type NetworkInfo,
} from "./data/supervisor";
import { LandingPageBaseElement } from "./landing-page-base-element";
export const ASSUME_CORE_START_SECONDS = 60;
const SCHEDULE_CORE_CHECK_SECONDS = 1;
@@ -94,16 +97,21 @@ class HaLandingPage extends LandingPageBaseElement {
<ha-language-picker
.value=${this.language}
.label=${""}
button-style
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<a
<ha-button
appearance="plain"
variant="neutral"
href="https://www.home-assistant.io/getting-started/onboarding/"
target="_blank"
rel="noreferrer noopener"
>${this.localize("ui.panel.page-onboarding.help")}</a
>
${this.localize("ui.panel.page-onboarding.help")}
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-button>
</div>
`;
}
@@ -218,26 +226,8 @@ class HaLandingPage extends LandingPageBaseElement {
ha-alert p {
text-align: unset;
}
ha-language-picker {
display: block;
width: 200px;
border-radius: 4px;
overflow: hidden;
--ha-select-height: 40px;
--mdc-select-fill-color: none;
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
--mdc-select-ink-color: var(--primary-text-color, #212121);
--mdc-select-idle-line-color: transparent;
--mdc-select-hover-line-color: transparent;
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
--mdc-shape-small: 0;
}
a {
text-decoration: none;
color: var(--primary-text-color);
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
.footer ha-svg-icon {
--mdc-icon-size: var(--ha-space-5);
}
ha-fade-in {
min-height: calc(100vh - 64px - 88px);
+9
View File
@@ -0,0 +1,9 @@
dist/
src/
node_modules/
*.md
.git/
.gitignore
docker-compose.yaml
backend/ha-logs-proxy
backend/README.md
+47
View File
@@ -0,0 +1,47 @@
ARG BUILD_FROM
FROM $BUILD_FROM AS base
# Install dependencies
RUN apk add --no-cache \
bash \
jq \
curl \
go
# Install Home Assistant CLI
ARG BUILD_ARCH
ARG CLI_VERSION
RUN curl -Lso /usr/bin/ha \
"https://github.com/home-assistant/cli/releases/download/${CLI_VERSION}/ha_${BUILD_ARCH}" \
&& chmod a+x /usr/bin/ha
# Build Go backend
WORKDIR /app
COPY backend/go.mod backend/go.sum* ./
RUN go mod download || true
COPY backend/*.go ./
RUN CGO_ENABLED=0 go build -o ha-logs-proxy .
# Final stage
FROM $BUILD_FROM
# Install runtime dependencies
RUN apk add --no-cache \
bash \
jq \
curl
# Copy HA CLI from base
COPY --from=base /usr/bin/ha /usr/bin/ha
# Copy Go backend
COPY --from=base /app/ha-logs-proxy /usr/bin/ha-logs-proxy
WORKDIR /root
# Expose port for backend (5642 = LOGB)
EXPOSE 5642
# Default command
CMD ["/usr/bin/ha-logs-proxy"]
+113
View File
@@ -0,0 +1,113 @@
# Home Assistant CLI Docker Container
A simple multi-architecture Docker container with the Home Assistant CLI installed.
## Development Usage
The CLI container is integrated into the `script/develop-logs` workflow. Both flags are required:
```bash
# Start dev server with CLI container (requires remote_api add-on)
script/develop-logs -c http://192.168.1.2 -t your_token_here
```
When started with credentials, the container runs a Go backend that proxies HA CLI logs commands. The backend API is available at `http://localhost:5642`.
**Frontend Features:**
- Dropdown menu to select log provider (core, supervisor, host, audio, dns, multicast)
- Follow mode with WebSocket streaming (`ha core logs --follow`)
- Manual refresh to fetch latest logs
- Download logs as text file
- Line wrapping toggle
- Auto-scroll to bottom when following
- Error display with retry
**API Endpoints:**
```bash
# List all endpoints
curl http://localhost:5642/api/logs
# Get static logs
curl http://localhost:5642/api/logs/core
curl http://localhost:5642/api/logs/supervisor
# Health check
curl http://localhost:5642/health
# WebSocket streaming (requires websocat or browser)
websocat ws://localhost:5642/api/logs/core/follow
```
You can also execute HA CLI commands directly:
```bash
docker exec -it ha-cli-dev ha info
docker exec -it ha-cli-dev ha supervisor info
```
Stop everything with Ctrl+C (both the dev server and backend will stop automatically).
### Getting API Token
1. Install the [remote_api add-on](https://github.com/home-assistant/addons/tree/master/remote_api) in Home Assistant
2. Check the add-on logs for the generated token
3. Use the token with the `-t` flag
## Build
### Local Build (Single Architecture)
```bash
docker build \
--build-arg BUILD_FROM=alpine:3.22 \
--build-arg BUILD_ARCH=amd64 \
--build-arg CLI_VERSION=4.42.0 \
-t ha-cli:local \
.
```
### Multi-Architecture Build
The `build.yaml` configuration is designed for use with Home Assistant's build system. For local multi-arch builds, use Docker Buildx:
```bash
docker buildx build \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--build-arg BUILD_FROM=alpine:3.22 \
--build-arg CLI_VERSION=4.42.0 \
-t ha-cli:latest \
.
```
## Usage
### Run CLI Commands
```bash
docker run --rm ha-cli:local ha help
docker run --rm ha-cli:local ha supervisor info
```
### Interactive Shell
```bash
docker run -it --rm ha-cli:local
# Then run: ha <command>
```
### With Docker Compose
```bash
docker compose run --rm ha-cli ha help
```
## Update CLI Version
Edit the `CLI_VERSION` in `build.yaml` or pass it as a build argument:
```bash
docker build --build-arg CLI_VERSION=4.43.0 ...
```
Check for latest versions at: https://github.com/home-assistant/cli/releases
+1
View File
@@ -0,0 +1 @@
ha-logs-proxy
+108
View File
@@ -0,0 +1,108 @@
# HA Logs Proxy Backend
A Go backend that proxies Home Assistant CLI logs commands through a secure HTTP API.
## Features
- Only allows `logs` commands (security-restricted)
- GET endpoints for static logs
- WebSocket endpoints for streaming logs (follow mode)
- CORS enabled for frontend integration
- Simple JSON API
## API Endpoints
### GET /api/logs
List all available endpoints.
### GET /api/logs/core
Get Home Assistant core logs.
### GET /api/logs/supervisor
Get Supervisor logs.
### GET /api/logs/host
Get host system logs.
### GET /api/logs/audio
Get audio logs.
### GET /api/logs/dns
Get DNS logs.
### GET /api/logs/multicast
Get multicast logs.
### GET /health
Health check endpoint.
**Response format (all log endpoints):**
```json
{
"output": "log content here...",
"error": "error message if any"
}
```
### WS /api/logs/*/follow
WebSocket endpoints for streaming logs in real-time.
Available endpoints:
- `WS /api/logs/core/follow` - Stream core logs
- `WS /api/logs/supervisor/follow` - Stream supervisor logs
- `WS /api/logs/host/follow` - Stream host logs
- `WS /api/logs/audio/follow` - Stream audio logs
- `WS /api/logs/dns/follow` - Stream DNS logs
- `WS /api/logs/multicast/follow` - Stream multicast logs
Each WebSocket message contains a single log line as plain text. The connection streams output from `ha {component} logs --follow` command.
## Running Locally
```bash
cd backend
go run main.go
```
The server starts on port 5642 (LOGB) by default. Override with `PORT` environment variable:
```bash
PORT=3000 go run main.go
```
## Building
```bash
go build -o ha-logs-proxy
./ha-logs-proxy
```
## Testing
```bash
# List endpoints
curl http://localhost:5642/api/logs
# Get core logs
curl http://localhost:5642/api/logs/core
# Get supervisor logs
curl http://localhost:5642/api/logs/supervisor
# Health check
curl http://localhost:5642/health
```
## Docker Integration
The backend is designed to run in the same container as the HA CLI, sharing access to the `ha` command.
+5
View File
@@ -0,0 +1,5 @@
module github.com/home-assistant/frontend/logs/backend
go 1.24
require github.com/gorilla/websocket v1.5.3
+2
View File
@@ -0,0 +1,2 @@
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+372
View File
@@ -0,0 +1,372 @@
package main
import (
"bufio"
"encoding/json"
"log"
"net/http"
"os"
"os/exec"
"github.com/gorilla/websocket"
)
type LogsResponse struct {
Output string `json:"output"`
Error string `json:"error,omitempty"`
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // Allow all origins (CORS)
},
}
func executeHACommand(args []string) (string, error) {
cmd := exec.Command("ha", args...)
output, err := cmd.CombinedOutput()
return string(output), err
}
func streamHACommandToWS(conn *websocket.Conn, args []string) error {
cmd := exec.Command("ha", args...)
// Get stdout pipe
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
// Start command
if err := cmd.Start(); err != nil {
return err
}
// Read and send output line by line
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
line := scanner.Text()
if err := conn.WriteMessage(websocket.TextMessage, []byte(line)); err != nil {
cmd.Process.Kill()
return err
}
}
// Wait for command to finish
if err := cmd.Wait(); err != nil {
return err
}
return scanner.Err()
}
func handleCoreLogs(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Execute ha core logs command
output, err := executeHACommand([]string{"core", "logs"})
response := LogsResponse{
Output: output,
}
if err != nil {
response.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Printf("Error encoding response: %v", err)
}
}
func handleSupervisorLogs(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
output, err := executeHACommand([]string{"supervisor", "logs"})
response := LogsResponse{
Output: output,
}
if err != nil {
response.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")
json.NewEncoder(w).Encode(response)
}
func handleHostLogs(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
output, err := executeHACommand([]string{"host", "logs"})
response := LogsResponse{
Output: output,
}
if err != nil {
response.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")
json.NewEncoder(w).Encode(response)
}
func handleAudioLogs(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
output, err := executeHACommand([]string{"audio", "logs"})
response := LogsResponse{
Output: output,
}
if err != nil {
response.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")
json.NewEncoder(w).Encode(response)
}
func handleDNSLogs(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
output, err := executeHACommand([]string{"dns", "logs"})
response := LogsResponse{
Output: output,
}
if err != nil {
response.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")
json.NewEncoder(w).Encode(response)
}
func handleMulticastLogs(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
output, err := executeHACommand([]string{"multicast", "logs"})
response := LogsResponse{
Output: output,
}
if err != nil {
response.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")
json.NewEncoder(w).Encode(response)
}
func handleCoreLogsFollow(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade failed: %v", err)
return
}
defer conn.Close()
log.Println("Client connected to core logs follow")
if err := streamHACommandToWS(conn, []string{"core", "logs", "--follow"}); err != nil {
log.Printf("Error streaming core logs: %v", err)
}
log.Println("Client disconnected from core logs follow")
}
func handleSupervisorLogsFollow(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade failed: %v", err)
return
}
defer conn.Close()
log.Println("Client connected to supervisor logs follow")
if err := streamHACommandToWS(conn, []string{"supervisor", "logs", "--follow"}); err != nil {
log.Printf("Error streaming supervisor logs: %v", err)
}
log.Println("Client disconnected from supervisor logs follow")
}
func handleHostLogsFollow(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade failed: %v", err)
return
}
defer conn.Close()
log.Println("Client connected to host logs follow")
if err := streamHACommandToWS(conn, []string{"host", "logs", "--follow"}); err != nil {
log.Printf("Error streaming host logs: %v", err)
}
log.Println("Client disconnected from host logs follow")
}
func handleAudioLogsFollow(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade failed: %v", err)
return
}
defer conn.Close()
log.Println("Client connected to audio logs follow")
if err := streamHACommandToWS(conn, []string{"audio", "logs", "--follow"}); err != nil {
log.Printf("Error streaming audio logs: %v", err)
}
log.Println("Client disconnected from audio logs follow")
}
func handleDNSLogsFollow(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade failed: %v", err)
return
}
defer conn.Close()
log.Println("Client connected to dns logs follow")
if err := streamHACommandToWS(conn, []string{"dns", "logs", "--follow"}); err != nil {
log.Printf("Error streaming dns logs: %v", err)
}
log.Println("Client disconnected from dns logs follow")
}
func handleMulticastLogsFollow(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade failed: %v", err)
return
}
defer conn.Close()
log.Println("Client connected to multicast logs follow")
if err := streamHACommandToWS(conn, []string{"multicast", "logs", "--follow"}); err != nil {
log.Printf("Error streaming multicast logs: %v", err)
}
log.Println("Client disconnected from multicast logs follow")
}
func listEndpoints(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
endpoints := map[string]string{
"core": "/api/logs/core",
"supervisor": "/api/logs/supervisor",
"host": "/api/logs/host",
"audio": "/api/logs/audio",
"dns": "/api/logs/dns",
"multicast": "/api/logs/multicast",
"health": "/health",
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
json.NewEncoder(w).Encode(endpoints)
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "5642"
}
// Register handlers
http.HandleFunc("/api/logs", listEndpoints)
http.HandleFunc("/api/logs/core", handleCoreLogs)
http.HandleFunc("/api/logs/supervisor", handleSupervisorLogs)
http.HandleFunc("/api/logs/host", handleHostLogs)
http.HandleFunc("/api/logs/audio", handleAudioLogs)
http.HandleFunc("/api/logs/dns", handleDNSLogs)
http.HandleFunc("/api/logs/multicast", handleMulticastLogs)
// WebSocket follow endpoints
http.HandleFunc("/api/logs/core/follow", handleCoreLogsFollow)
http.HandleFunc("/api/logs/supervisor/follow", handleSupervisorLogsFollow)
http.HandleFunc("/api/logs/host/follow", handleHostLogsFollow)
http.HandleFunc("/api/logs/audio/follow", handleAudioLogsFollow)
http.HandleFunc("/api/logs/dns/follow", handleDNSLogsFollow)
http.HandleFunc("/api/logs/multicast/follow", handleMulticastLogsFollow)
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
log.Printf("Starting HA Logs Proxy on port %s", port)
log.Printf("Available endpoints:")
log.Printf(" GET /api/logs - List all endpoints")
log.Printf(" GET /api/logs/core - Core logs")
log.Printf(" GET /api/logs/supervisor - Supervisor logs")
log.Printf(" GET /api/logs/host - Host logs")
log.Printf(" GET /api/logs/audio - Audio logs")
log.Printf(" GET /api/logs/dns - DNS logs")
log.Printf(" GET /api/logs/multicast - Multicast logs")
log.Printf(" WS /api/logs/*/follow - Stream logs (WebSocket)")
log.Printf(" GET /health - Health check")
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}
+19
View File
@@ -0,0 +1,19 @@
---
image: ghcr.io/home-assistant/{arch}-hassio-cli
build_from:
aarch64: ghcr.io/home-assistant/aarch64-base:3.22
armhf: ghcr.io/home-assistant/armhf-base:3.22
armv7: ghcr.io/home-assistant/armv7-base:3.22
amd64: ghcr.io/home-assistant/amd64-base:3.22
i386: ghcr.io/home-assistant/i386-base:3.22
args:
CLI_VERSION: 4.42.0
cosign:
enabled: true
repository_name: home-assistant/cli
repository_owner: home-assistant
labels:
org.opencontainers.image.title: Home Assistant CLI
org.opencontainers.image.description: Home Assistant CLI for container environments
org.opencontainers.image.source: https://github.com/home-assistant/cli
org.opencontainers.image.licenses: Apache-2.0
+16
View File
@@ -0,0 +1,16 @@
---
services:
ha-cli:
build:
context: .
args:
BUILD_FROM: alpine:3.22
BUILD_ARCH: amd64
CLI_VERSION: 4.42.0
image: ha-cli:local
ports:
- "5642:5642"
environment:
- PORT=5642
stdin_open: true
tty: true
+34
View File
@@ -0,0 +1,34 @@
import { darkSemanticColorStyles } from "../../src/resources/theme/color/semantic.globals";
import { darkColorStyles } from "../../src/resources/theme/color/color.globals";
const mql = matchMedia("(prefers-color-scheme: dark)");
function applyTheme(dark: boolean) {
const el = document.documentElement;
if (dark) {
el.setAttribute("dark", "");
} else {
el.removeAttribute("dark");
}
}
// Add dark theme styles wrapped in media query
// This runs after append-ha-style has loaded the base theme
const styleElement = document.createElement("style");
styleElement.id = "auto-theme-dark";
styleElement.textContent = `
@media (prefers-color-scheme: dark) {
${darkSemanticColorStyles.cssText}
${darkColorStyles.cssText}
}
`;
// Append to head to ensure it comes after base styles
document.head.appendChild(styleElement);
// Apply theme on initial load
applyTheme(mql.matches);
// Listen for theme changes
mql.addEventListener("change", (e) => {
applyTheme(e.matches);
});
+8
View File
@@ -0,0 +1,8 @@
import "./logs-app";
// Load base styles first, then apply theme
import("../../src/resources/append-ha-style").then(() => {
import("./auto-theme");
});
document.body.appendChild(document.createElement("logs-app"));
+37
View File
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#03a9f4" />
<meta name="color-scheme" content="dark light" />
<title>Home Assistant Logs</title>
<% for (const entry of latestEntryJS) { %>
<script type="module" src="<%= entry %>"></script>
<% } %>
<style>
html {
background-color: var(--primary-background-color, #fafafa);
color: var(--primary-text-color, #212121);
}
@media (prefers-color-scheme: dark) {
html {
background-color: var(--primary-background-color, #111111);
color: var(--primary-text-color, #e1e1e1);
}
}
body {
font-family: Roboto, Noto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-weight: 400;
margin: 0;
padding: 0;
}
</style>
</head>
<body></body>
</html>
+24
View File
@@ -0,0 +1,24 @@
import { LitElement, css, html } from "lit";
import { customElement } from "lit/decorators";
import "./logs-viewer";
@customElement("logs-app")
class LogsApp extends LitElement {
render() {
return html`<logs-viewer></logs-viewer>`;
}
static styles = css`
:host {
display: block;
min-height: 100vh;
background-color: var(--primary-background-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"logs-app": LogsApp;
}
}
+538
View File
@@ -0,0 +1,538 @@
import {
mdiArrowCollapseDown,
mdiChevronDown,
mdiCircle,
mdiDownload,
mdiRefresh,
mdiWrap,
mdiWrapDisabled,
} from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
// eslint-disable-next-line import/extensions
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
import "../../src/components/ha-ansi-to-html";
import type { HaAnsiToHtml } from "../../src/components/ha-ansi-to-html";
import "../../src/components/ha-button";
import "../../src/components/ha-button-menu";
import "../../src/components/ha-card";
import "../../src/components/ha-icon-button";
import "../../src/components/ha-list-item";
import "../../src/components/ha-spinner";
import "../../src/components/ha-svg-icon";
// Data types
interface LogProvider {
key: string;
name: string;
}
@customElement("logs-viewer")
export class LogsViewer extends LitElement {
@property({ type: Boolean }) public narrow = false;
@state() private _selectedLogProvider?: string;
@state() private _logProviders: LogProvider[] = [];
@state() private _loading = false;
@state() private _wrapLines = true;
@state() private _error?: string;
@state() private _newLogsIndicator?: boolean;
@query(".error-log") private _logElement?: HTMLElement;
@query("#scroll-top-marker") private _scrollTopMarkerElement?: HTMLElement;
@query("#scroll-bottom-marker")
private _scrollBottomMarkerElement?: HTMLElement;
@query("ha-ansi-to-html") private _ansiToHtmlElement?: HaAnsiToHtml;
@state() private _scrolledToBottomController =
new IntersectionController<boolean>(this, {
callback(this: IntersectionController<boolean>, entries) {
return entries[0].isIntersecting;
},
});
@state() private _scrolledToTopController =
new IntersectionController<boolean>(this, {});
private _ws: WebSocket | null = null;
private _apiUrl = `http://${window.location.hostname}:5642`;
private async _fetchLogs(): Promise<void> {
if (!this._selectedLogProvider) {
return;
}
this._loading = true;
this._error = undefined;
// Stop any existing websocket
this._stopFollowing();
// Clear existing logs
this._ansiToHtmlElement?.clear();
try {
// First, fetch the latest logs
const response = await fetch(
`${this._apiUrl}/api/logs/${this._selectedLogProvider}`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
const logText = data.output || "";
// Parse and display initial logs
if (logText.trim()) {
this._ansiToHtmlElement?.parseTextToColoredPre(logText);
// Add divider line
this._ansiToHtmlElement?.parseLineToColoredPre(
"--- Live logs start here ---"
);
}
this._loading = false;
// Scroll to bottom after loading
this._scrollToBottom();
// Start streaming
this._startFollowing();
} catch (err) {
this._error = `Error loading logs: ${err}`;
this._loading = false;
// eslint-disable-next-line
console.error("Error fetching logs:", err);
}
}
private async _fetchLogProviders(): Promise<void> {
try {
const response = await fetch(`${this._apiUrl}/api/logs`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const providers = await response.json();
// Define the order (matching backend registration order)
const order = ["core", "supervisor", "host", "audio", "dns", "multicast"];
// Convert object to array of providers, filter out health endpoint, and sort
this._logProviders = Object.entries(providers)
.filter(([key]) => key !== "health")
.map(([key]) => ({
key,
name: key.charAt(0).toUpperCase() + key.slice(1),
}))
.sort((a, b) => order.indexOf(a.key) - order.indexOf(b.key));
// Set default provider once loaded
if (this._logProviders.length > 0 && !this._selectedLogProvider) {
this._selectedLogProvider = this._logProviders[0].key;
await this._fetchLogs();
}
} catch (err) {
this._error = `Failed to load log providers: ${err}`;
// eslint-disable-next-line
console.error("Error fetching log providers:", err);
}
}
connectedCallback() {
super.connectedCallback();
this._fetchLogProviders();
}
disconnectedCallback() {
super.disconnectedCallback();
this._stopFollowing();
}
protected firstUpdated() {
this._scrolledToBottomController.observe(this._scrollBottomMarkerElement!);
this._scrolledToTopController.observe(this._scrollTopMarkerElement!);
}
protected updated() {
if (this._newLogsIndicator && this._scrolledToBottomController.value) {
this._newLogsIndicator = false;
}
}
private _selectProvider(ev: Event) {
const target = ev.currentTarget as any;
this._selectedLogProvider = target.provider;
this._fetchLogs();
}
private _refresh() {
this._fetchLogs();
}
private _toggleLineWrap() {
this._wrapLines = !this._wrapLines;
}
private _scrollToBottom(): void {
if (this._logElement) {
this._newLogsIndicator = false;
this._logElement.scrollTo(0, this._logElement.scrollHeight);
}
}
private _startFollowing() {
if (!this._selectedLogProvider) {
return;
}
this._stopFollowing();
this._error = undefined;
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${wsProtocol}//${window.location.hostname}:5642/api/logs/${this._selectedLogProvider}/follow`;
try {
this._ws = new WebSocket(wsUrl);
this._ws.onopen = () => {
// eslint-disable-next-line
console.log("WebSocket connected");
};
this._ws.onmessage = (event) => {
const scrolledToBottom = this._scrolledToBottomController.value;
// Add the new line to the display
this._ansiToHtmlElement?.parseLineToColoredPre(event.data);
// Auto-scroll if user is at bottom
if (scrolledToBottom && this._logElement) {
this._scrollToBottom();
} else {
this._newLogsIndicator = true;
}
};
this._ws.onerror = (error) => {
// eslint-disable-next-line
console.error("WebSocket error:", error);
this._error = "WebSocket connection error";
};
this._ws.onclose = () => {
// eslint-disable-next-line
console.log("WebSocket disconnected");
};
} catch (err) {
this._error = `Failed to start following logs: ${err}`;
// eslint-disable-next-line
console.error("Error starting WebSocket:", err);
}
}
private _stopFollowing() {
if (this._ws) {
this._ws.close();
this._ws = null;
}
}
private _downloadLogs() {
if (!this._selectedLogProvider || !this._ansiToHtmlElement) {
return;
}
// Get the text content from the logs
const logText =
this._ansiToHtmlElement.shadowRoot?.querySelector("pre")?.textContent ||
"";
if (!logText.trim()) {
return;
}
// Create blob from log text
const blob = new Blob([logText], { type: "text/plain" });
const url = URL.createObjectURL(blob);
// Create download link and trigger it
const a = document.createElement("a");
a.href = url;
a.download = `${this._selectedLogProvider}-logs-${Date.now()}.txt`;
document.body.appendChild(a);
a.click();
// Cleanup
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
render() {
const currentProvider = this._logProviders.find(
(p) => p.key === this._selectedLogProvider
);
return html`
<div class="container">
<div class="toolbar">
<ha-button-menu>
<ha-button slot="trigger" appearance="filled">
<ha-svg-icon slot="end" .path=${mdiChevronDown}></ha-svg-icon>
${currentProvider?.name || "Select Provider"}
</ha-button>
${this._logProviders.map(
(provider) => html`
<ha-list-item
?selected=${provider.key === this._selectedLogProvider}
.provider=${provider.key}
@click=${this._selectProvider}
>
${provider.name}
</ha-list-item>
`
)}
</ha-button-menu>
</div>
<div class="content">
<div class="error-log-intro">
<ha-card outlined>
<div class="header">
<h1 class="card-header">${currentProvider?.name || "Logs"}</h1>
<div class="action-buttons">
<ha-icon-button
.path=${mdiDownload}
@click=${this._downloadLogs}
.label=${"Download logs"}
.disabled=${!this._ansiToHtmlElement}
></ha-icon-button>
<ha-icon-button
.path=${this._wrapLines ? mdiWrapDisabled : mdiWrap}
@click=${this._toggleLineWrap}
.label=${this._wrapLines ? "Full width" : "Wrap lines"}
></ha-icon-button>
<ha-icon-button
.path=${mdiRefresh}
@click=${this._refresh}
.label=${"Refresh"}
.disabled=${!this._selectedLogProvider}
></ha-icon-button>
</div>
</div>
<div class="card-content error-log">
<div id="scroll-top-marker"></div>
${this._loading
? html`<div>Loading logs...</div>`
: this._error
? html`<div class="error">${this._error}</div>`
: nothing}
<ha-ansi-to-html
?wrap-disabled=${!this._wrapLines}
></ha-ansi-to-html>
<div id="scroll-bottom-marker"></div>
</div>
<ha-button
class="new-logs-indicator ${classMap({
visible:
(this._newLogsIndicator &&
!this._scrolledToBottomController.value) ||
false,
})}"
size="small"
appearance="filled"
@click=${this._scrollToBottom}
>
<ha-svg-icon
.path=${mdiArrowCollapseDown}
slot="start"
></ha-svg-icon>
Scroll down
<ha-svg-icon
.path=${mdiArrowCollapseDown}
slot="end"
></ha-svg-icon>
</ha-button>
${this._ws && !this._error
? html`<div class="live-indicator">
<ha-svg-icon .path=${mdiCircle}></ha-svg-icon>
Live
</div>`
: nothing}
</ha-card>
</div>
</div>
</div>
`;
}
static styles: CSSResultGroup = css`
:host {
display: block;
direction: var(--direction);
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.toolbar {
padding: var(--ha-space-2) var(--ha-space-4);
display: flex;
justify-content: flex-end;
gap: var(--ha-space-2);
}
.content {
direction: ltr;
}
.error-log-intro {
text-align: center;
margin: 0 var(--ha-space-4);
}
ha-card {
padding-top: var(--ha-space-2);
position: relative;
}
.header {
display: flex;
justify-content: space-between;
padding: 0 var(--ha-space-4);
}
.action-buttons {
display: flex;
align-items: center;
height: 100%;
}
.card-header {
color: var(--ha-card-header-color, var(--primary-text-color));
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
letter-spacing: -0.012em;
line-height: var(--ha-line-height-expanded);
display: block;
margin-block-start: 0px;
font-weight: var(--ha-font-weight-normal);
white-space: nowrap;
max-width: calc(100% - 150px);
overflow: hidden;
text-overflow: ellipsis;
}
.error-log {
position: relative;
font-family: var(--ha-font-family-code);
clear: both;
text-align: start;
padding-top: var(--ha-space-4);
padding-bottom: var(--ha-space-4);
overflow-y: scroll;
min-height: var(--error-log-card-height, calc(100vh - 244px));
max-height: var(--error-log-card-height, calc(100vh - 244px));
border-top: 1px solid var(--divider-color);
direction: ltr;
}
.error-log > div {
padding: 0 var(--ha-space-4);
overflow: auto;
}
.error {
color: var(--error-color);
padding: var(--ha-space-4);
}
.new-logs-indicator {
overflow: hidden;
position: absolute;
bottom: var(--ha-space-1);
left: var(--ha-space-1);
height: 0;
transition: height 0.4s ease-out;
}
.new-logs-indicator.visible {
height: 32px;
}
@keyframes breathe {
from {
opacity: 0.8;
}
to {
opacity: 0;
}
}
.live-indicator {
position: absolute;
bottom: 0;
inset-inline-end: var(--ha-space-4);
border-top-right-radius: var(--ha-space-2);
border-top-left-radius: var(--ha-space-2);
background-color: var(--primary-color);
color: var(--text-primary-color);
padding: var(--ha-space-1) var(--ha-space-2);
opacity: 0.8;
}
.live-indicator ha-svg-icon {
animation: breathe 1s cubic-bezier(0.5, 0, 1, 1) infinite alternate;
height: 14px;
width: 14px;
}
@media all and (max-width: 870px) {
.error-log {
min-height: var(--error-log-card-height, calc(100vh - 190px));
max-height: var(--error-log-card-height, calc(100vh - 190px));
}
ha-button-menu {
max-width: 50%;
}
ha-button {
max-width: 100%;
}
ha-button::part(label) {
overflow: hidden;
white-space: nowrap;
}
}
ha-list-item[selected] {
color: var(--primary-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"logs-viewer": LogsViewer;
}
}
+52 -49
View File
@@ -28,32 +28,32 @@
"dependencies": {
"@babel/runtime": "7.28.4",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.19.0",
"@codemirror/commands": "6.8.1",
"@codemirror/autocomplete": "6.19.1",
"@codemirror/commands": "6.10.0",
"@codemirror/language": "6.11.3",
"@codemirror/legacy-modes": "6.5.1",
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.38.3",
"@codemirror/view": "6.38.6",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.0",
"@formatjs/intl-displaynames": "6.8.11",
"@formatjs/intl-durationformat": "0.7.4",
"@formatjs/intl-getcanonicallocales": "2.5.5",
"@formatjs/intl-listformat": "7.7.11",
"@formatjs/intl-locale": "4.2.11",
"@formatjs/intl-numberformat": "8.15.4",
"@formatjs/intl-pluralrules": "5.4.4",
"@formatjs/intl-relativetimeformat": "11.4.11",
"@formatjs/intl-datetimeformat": "6.18.2",
"@formatjs/intl-displaynames": "6.8.13",
"@formatjs/intl-durationformat": "0.7.6",
"@formatjs/intl-getcanonicallocales": "2.5.6",
"@formatjs/intl-listformat": "7.7.13",
"@formatjs/intl-locale": "4.2.13",
"@formatjs/intl-numberformat": "8.15.6",
"@formatjs/intl-pluralrules": "5.4.6",
"@formatjs/intl-relativetimeformat": "11.4.13",
"@fullcalendar/core": "6.1.19",
"@fullcalendar/daygrid": "6.1.19",
"@fullcalendar/interaction": "6.1.19",
"@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@home-assistant/webawesome": "3.0.0-beta.4.ha.3",
"@lezer/highlight": "1.2.1",
"@home-assistant/webawesome": "3.0.0-beta.6.ha.7",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6",
"@lit-labs/virtualizer": "2.1.1",
@@ -81,7 +81,7 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "2.4.0",
"@material/web": "2.4.1",
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@replit/codemirror-indentation-markers": "6.5.3",
@@ -89,17 +89,17 @@
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.9.1",
"@vaadin/vaadin-themable-mixin": "24.9.1",
"@vaadin/combo-box": "24.9.4",
"@vaadin/vaadin-themable-mixin": "24.9.4",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "3.0.5",
"barcode-detector": "3.0.6",
"color-name": "2.0.2",
"comlink": "4.4.2",
"core-js": "3.45.1",
"core-js": "3.46.0",
"cropperjs": "1.6.2",
"culori": "4.0.2",
"date-fns": "4.1.0",
@@ -111,10 +111,10 @@
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"hls.js": "1.6.13",
"hls.js": "1.6.14",
"home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "10.7.16",
"intl-messageformat": "10.7.18",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -122,7 +122,7 @@
"lit": "3.3.1",
"lit-html": "3.3.1",
"luxon": "3.7.2",
"marked": "16.3.0",
"marked": "16.4.1",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
@@ -135,7 +135,7 @@
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
"ua-parser-js": "2.0.5",
"ua-parser-js": "2.0.6",
"vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
@@ -148,17 +148,17 @@
"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",
"@bundle-stats/plugin-webpack-filter": "4.21.3",
"@lokalise/node-api": "15.2.1",
"@octokit/auth-oauth-device": "8.0.1",
"@octokit/plugin-retry": "8.0.1",
"@octokit/rest": "22.0.0",
"@rsdoctor/rspack-plugin": "1.3.0",
"@rspack/core": "1.5.7",
"@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.5",
"@bundle-stats/plugin-webpack-filter": "4.21.6",
"@lokalise/node-api": "15.3.1",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.3.7",
"@rspack/core": "1.6.0",
"@rspack/dev-server": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.22",
@@ -167,31 +167,31 @@
"@types/culori": "4.0.1",
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.20",
"@types/leaflet": "1.9.21",
"@types/leaflet-draw": "1.0.13",
"@types/leaflet.markercluster": "1.5.6",
"@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": "3.2.4",
"@vitest/coverage-v8": "4.0.6",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.1",
"eslint": "9.36.0",
"eslint": "9.39.1",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.10",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-lit": "2.1.1",
"eslint-plugin-lit-a11y": "5.1.1",
"eslint-plugin-unused-imports": "4.2.0",
"eslint-plugin-wc": "3.0.1",
"eslint-plugin-unused-imports": "4.3.0",
"eslint-plugin-wc": "3.0.2",
"fancy-log": "2.0.0",
"fs-extra": "11.3.2",
"glob": "11.0.3",
@@ -201,9 +201,9 @@
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "27.0.0",
"jsdom": "27.1.0",
"jszip": "3.10.1",
"lint-staged": "16.2.1",
"lint-staged": "16.2.6",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
@@ -213,13 +213,13 @@
"rspack-manifest-plugin": "5.1.0",
"serve": "14.2.5",
"sinon": "21.0.0",
"tar": "7.5.1",
"tar": "7.5.2",
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.2",
"typescript-eslint": "8.44.1",
"typescript": "5.9.3",
"typescript-eslint": "8.46.3",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.2.4",
"vitest": "4.0.6",
"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"
@@ -231,9 +231,12 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.1",
"@fullcalendar/daygrid": "6.1.19",
"globals": "16.4.0",
"globals": "16.5.0",
"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": "[email protected]"
"packageManager": "[email protected]",
"volta": {
"node": "22.21.1"
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20250924.0"
version = "20251029.0"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"
+1 -1
View File
@@ -9,7 +9,7 @@
":semanticCommitsDisabled",
"group:monorepos",
"group:recommended",
"npm:unpublishSafe"
"security:minimumReleaseAgeNpm"
],
"enabledManagers": ["npm", "nvm"],
"postUpdateOptions": ["yarnDedupeHighest"],
+96
View File
@@ -0,0 +1,96 @@
#!/bin/sh
# Run the logs frontend development server
# Stop on errors
set -e
cd "$(dirname "$0")/.."
# Parse command line arguments
SUPERVISOR_ENDPOINT=""
SUPERVISOR_API_TOKEN=""
while getopts "c:t:h" opt; do
case $opt in
c)
SUPERVISOR_ENDPOINT="$OPTARG"
;;
t)
SUPERVISOR_API_TOKEN="$OPTARG"
;;
h)
echo "Usage: $0 -c SUPERVISOR_ENDPOINT -t SUPERVISOR_API_TOKEN"
echo ""
echo "Options:"
echo " -c SUPERVISOR_ENDPOINT (e.g., http://192.168.1.2) [required]"
echo " -t SUPERVISOR_API_TOKEN (from remote_api add-on) [required]"
echo " -h Show this help message"
echo ""
echo "Example:"
echo " $0 -c http://192.168.1.2 -t your_token_here"
exit 0
;;
\?)
echo "Invalid option: -$OPTARG" >&2
echo "Use -h for help"
exit 1
;;
esac
done
# Validate that both -c and -t are provided
if [ -z "$SUPERVISOR_ENDPOINT" ] || [ -z "$SUPERVISOR_API_TOKEN" ]; then
echo "Error: Both -c and -t are required" >&2
echo "Use -h for help"
exit 1
fi
# Cleanup function
cleanup() {
echo ""
echo "Shutting down..."
echo "Stopping HA CLI container..."
docker stop ha-cli-dev 2>/dev/null || true
exit 0
}
# Set up trap to cleanup on exit
trap cleanup INT TERM EXIT
# Run HA CLI container
echo "Starting HA CLI container..."
# Build the container if needed
if ! docker images | grep -q "ha-cli:local"; then
echo "Building HA CLI container..."
(cd logs && docker compose build)
fi
# Clean up any existing container
docker stop ha-cli-dev 2>/dev/null || true
# Run the container in background (not detached, so it shares stdout)
docker run \
--name ha-cli-dev \
--rm \
-p 5642:5642 \
-e SUPERVISOR_ENDPOINT="$SUPERVISOR_ENDPOINT" \
-e SUPERVISOR_API_TOKEN="$SUPERVISOR_API_TOKEN" \
-e PORT=5642 \
ha-cli:local &
# Store the docker process ID
DOCKER_PID=$!
# Wait a moment for container to start
sleep 2
echo ""
echo "HA Logs Backend API: http://localhost:5642"
echo " GET /api/logs - List endpoints"
echo " GET /api/logs/core - Core logs"
echo " GET /api/logs/supervisor - Supervisor logs"
echo " GET /health - Health check"
echo ""
# Run gulp (this will block until Ctrl+C)
./node_modules/.bin/gulp develop-logs
+16 -22
View File
@@ -1,4 +1,5 @@
/* eslint-disable lit/prefer-static-styles */
import { mdiOpenInNew } from "@mdi/js";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -6,6 +7,8 @@ import punycode from "punycode";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { extractSearchParamsObject } from "../common/url/search-params";
import "../components/ha-alert";
import "../components/ha-button";
import "../components/ha-svg-icon";
import type { AuthProvider, AuthUrlSearchParams } from "../data/auth";
import { fetchAuthProviders } from "../data/auth";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
@@ -103,7 +106,10 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
);
box-shadow: var(--ha-card-box-shadow, none);
box-sizing: border-box;
border-radius: var(--ha-card-border-radius, 12px);
border-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
border-width: var(--ha-card-border-width, 1px);
border-style: solid;
border-color: var(
@@ -130,25 +136,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
justify-content: space-between;
align-items: center;
}
ha-language-picker {
width: 200px;
border-radius: 4px;
overflow: hidden;
--ha-select-height: 40px;
--mdc-select-fill-color: none;
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
--mdc-select-ink-color: var(--primary-text-color, #212121);
--mdc-select-idle-line-color: transparent;
--mdc-select-hover-line-color: transparent;
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
--mdc-shape-small: 0;
}
.footer a {
text-decoration: none;
color: var(--primary-text-color);
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
.footer ha-svg-icon {
--mdc-icon-size: var(--ha-space-5);
}
h1 {
font-size: var(--ha-font-size-3xl);
@@ -202,16 +191,21 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
<ha-language-picker
.value=${this.language}
.label=${""}
button-style
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<a
<ha-button
appearance="plain"
variant="neutral"
href="https://www.home-assistant.io/docs/authentication/"
target="_blank"
rel="noreferrer noopener"
>${this.localize("ui.panel.page-authorize.help")}</a
>
${this.localize("ui.panel.page-authorize.help")}
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-button>
</div>
`;
}
@@ -0,0 +1,141 @@
import type {
ReactiveController,
ReactiveControllerHost,
} from "@lit/reactive-element/reactive-controller";
const UNDO_REDO_STACK_LIMIT = 75;
/**
* Configuration options for the UndoRedoController.
*
* @template ConfigType The type of configuration to manage.
*/
export interface UndoRedoControllerConfig<ConfigType> {
stackLimit?: number;
currentConfig: () => ConfigType;
apply: (config: ConfigType) => void;
}
/**
* A controller to manage undo and redo operations for a given configuration type.
*
* @template ConfigType The type of configuration to manage.
*/
export class UndoRedoController<ConfigType> implements ReactiveController {
private _host: ReactiveControllerHost;
private _undoStack: ConfigType[] = [];
private _redoStack: ConfigType[] = [];
private readonly _stackLimit: number = UNDO_REDO_STACK_LIMIT;
private readonly _apply: (config: ConfigType) => void = () => {
throw new Error("No apply function provided");
};
private readonly _currentConfig: () => ConfigType = () => {
throw new Error("No currentConfig function provided");
};
constructor(
host: ReactiveControllerHost,
options: UndoRedoControllerConfig<ConfigType>
) {
if (options.stackLimit !== undefined) {
this._stackLimit = options.stackLimit;
}
this._apply = options.apply;
this._currentConfig = options.currentConfig;
this._host = host;
host.addController(this);
}
hostConnected() {
window.addEventListener("undo-change", this._onUndoChange);
}
hostDisconnected() {
window.removeEventListener("undo-change", this._onUndoChange);
}
private _onUndoChange = (ev: Event) => {
ev.stopPropagation();
this.undo();
this._host.requestUpdate();
};
/**
* Indicates whether there are actions available to undo.
*
* @returns `true` if there are actions to undo, `false` otherwise.
*/
public get canUndo(): boolean {
return this._undoStack.length > 0;
}
/**
* Indicates whether there are actions available to redo.
*
* @returns `true` if there are actions to redo, `false` otherwise.
*/
public get canRedo(): boolean {
return this._redoStack.length > 0;
}
/**
* Commits the current configuration to the undo stack and clears the redo stack.
*
* @param config The current configuration to commit.
*/
public commit(config: ConfigType) {
if (this._undoStack.length >= this._stackLimit) {
this._undoStack.shift();
}
this._undoStack.push({ ...config });
this._redoStack = [];
}
/**
* Undoes the last action and applies the previous configuration
* while saving the current configuration to the redo stack.
*/
public undo() {
if (this._undoStack.length === 0) {
return;
}
this._redoStack.push({ ...this._currentConfig() });
const config = this._undoStack.pop()!;
this._apply(config);
this._host.requestUpdate();
}
/**
* Redoes the last undone action and reapplies the configuration
* while saving the current configuration to the undo stack.
*/
public redo() {
if (this._redoStack.length === 0) {
return;
}
this._undoStack.push({ ...this._currentConfig() });
const config = this._redoStack.pop()!;
this._apply(config);
this._host.requestUpdate();
}
/**
* Resets the undo and redo stacks, clearing all history.
*/
public reset() {
this._undoStack = [];
this._redoStack = [];
}
}
declare global {
interface HASSDomEvents {
"undo-change": undefined;
}
}
+2 -2
View File
@@ -31,10 +31,10 @@ export const isNavigationClick = (e: MouseEvent, preventDefault = true) => {
const location = window.location;
const origin = location.origin || location.protocol + "//" + location.host;
if (href.indexOf(origin) !== 0) {
if (!href.startsWith(origin)) {
return undefined;
}
href = href.substr(origin.length);
href = href.slice(origin.length);
if (href === "#") {
return undefined;
+6
View File
@@ -61,3 +61,9 @@ export const computeEntityEntryName = (
return name;
};
export const entityUseDeviceName = (
stateObj: HassEntity,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"]
): boolean => !computeEntityName(stateObj, entities, devices);
@@ -0,0 +1,109 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../../types";
import { ensureArray } from "../array/ensure-array";
import { computeAreaName } from "./compute_area_name";
import { computeDeviceName } from "./compute_device_name";
import { computeEntityName, entityUseDeviceName } from "./compute_entity_name";
import { computeFloorName } from "./compute_floor_name";
import { getEntityContext } from "./context/get_entity_context";
const DEFAULT_SEPARATOR = " ";
export const DEFAULT_ENTITY_NAME = [
{ type: "device" },
{ type: "entity" },
] satisfies EntityNameItem[];
export type EntityNameItem =
| {
type: "entity" | "device" | "area" | "floor";
}
| {
type: "text";
text: string;
};
export interface EntityNameOptions {
separator?: string;
}
export const computeEntityNameDisplay = (
stateObj: HassEntity,
name: EntityNameItem | EntityNameItem[] | undefined,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
floors: HomeAssistant["floors"],
options?: EntityNameOptions
) => {
let items = ensureArray(name || DEFAULT_ENTITY_NAME);
const separator = options?.separator ?? DEFAULT_SEPARATOR;
// If all items are text, just join them
if (items.every((n) => n.type === "text")) {
return items.map((item) => item.text).join(separator);
}
const useDeviceName = entityUseDeviceName(stateObj, entities, devices);
// If entity uses device name, and device is not already included, replace it with device name
if (useDeviceName) {
const hasDevice = items.some((n) => n.type === "device");
if (!hasDevice) {
items = items.map((n) => (n.type === "entity" ? { type: "device" } : n));
}
}
const names = computeEntityNameList(
stateObj,
items,
entities,
devices,
areas,
floors
);
// If after processing there is only one name, return that
if (names.length === 1) {
return names[0] || "";
}
return names.filter((n) => n).join(separator);
};
export const computeEntityNameList = (
stateObj: HassEntity,
name: EntityNameItem[],
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
floors: HomeAssistant["floors"]
): (string | undefined)[] => {
const { device, area, floor } = getEntityContext(
stateObj,
entities,
devices,
areas,
floors
);
const names = name.map((item) => {
switch (item.type) {
case "entity":
return computeEntityName(stateObj, entities, devices);
case "device":
return device ? computeDeviceName(device) : undefined;
case "area":
return area ? computeAreaName(area) : undefined;
case "floor":
return floor ? computeFloorName(floor) : undefined;
case "text":
return item.text;
default:
return "";
}
});
return names;
};
+1 -1
View File
@@ -1,3 +1,3 @@
/** Compute the object ID of a state. */
export const computeObjectId = (entityId: string): string =>
entityId.substr(entityId.indexOf(".") + 1);
entityId.slice(entityId.indexOf(".") + 1);
@@ -8,10 +8,10 @@ interface AreaContext {
}
export const getAreaContext = (
area: AreaRegistryEntry,
hass: HomeAssistant
hassFloors: HomeAssistant["floors"]
): AreaContext => {
const floorId = area.floor_id;
const floor = floorId ? hass.floors[floorId] : undefined;
const floor = floorId ? hassFloors[floorId] : undefined;
return {
area: area,
+43 -17
View File
@@ -9,9 +9,9 @@ type EntityCategory = "none" | "config" | "diagnostic";
export interface EntityFilter {
domain?: string | string[];
device_class?: string | string[];
device?: string | string[];
area?: string | string[];
floor?: string | string[];
device?: string | null | (string | null)[];
area?: string | null | (string | null)[];
floor?: string | null | (string | null)[];
label?: string | string[];
entity_category?: EntityCategory | EntityCategory[];
hidden_platform?: string | string[];
@@ -19,6 +19,18 @@ export interface EntityFilter {
export type EntityFilterFunc = (entityId: string) => boolean;
const normalizeFilterArray = <T>(
value: T | null | T[] | (T | null)[] | undefined
): Set<T | null> | undefined => {
if (value === undefined) {
return undefined;
}
if (value === null) {
return new Set([null]);
}
return new Set(ensureArray(value));
};
export const generateEntityFilter = (
hass: HomeAssistant,
filter: EntityFilter
@@ -29,11 +41,9 @@ export const generateEntityFilter = (
const deviceClasses = filter.device_class
? new Set(ensureArray(filter.device_class))
: undefined;
const floors = filter.floor ? new Set(ensureArray(filter.floor)) : undefined;
const areas = filter.area ? new Set(ensureArray(filter.area)) : undefined;
const devices = filter.device
? new Set(ensureArray(filter.device))
: undefined;
const floors = normalizeFilterArray(filter.floor);
const areas = normalizeFilterArray(filter.area);
const devices = normalizeFilterArray(filter.device);
const entityCategories = filter.entity_category
? new Set(ensureArray(filter.entity_category))
: undefined;
@@ -73,23 +83,20 @@ export const generateEntityFilter = (
}
if (floors) {
if (!floor || !floors.has(floor.floor_id)) {
const floorId = floor?.floor_id ?? null;
if (!floors.has(floorId)) {
return false;
}
}
if (areas) {
if (!area) {
return false;
}
if (!areas.has(area.area_id)) {
const areaId = area?.area_id ?? null;
if (!areas.has(areaId)) {
return false;
}
}
if (devices) {
if (!device) {
return false;
}
if (!devices.has(device.id)) {
const deviceId = device?.id ?? null;
if (!devices.has(deviceId)) {
return false;
}
}
@@ -122,3 +129,22 @@ export const generateEntityFilter = (
return true;
};
};
export const findEntities = (
entities: string[],
filters: EntityFilterFunc[]
): string[] => {
const seen = new Set<string>();
const results: string[] = [];
for (const filter of filters) {
for (const entity of entities) {
if (filter(entity) && !seen.has(entity)) {
seen.add(entity);
results.push(entity);
}
}
}
return results;
};
+2
View File
@@ -18,6 +18,7 @@ export const FIXED_DOMAIN_STATES = {
"pending",
"triggered",
],
alert: ["on", "off", "idle"],
assist_satellite: ["idle", "listening", "responding", "processing"],
automation: ["on", "off"],
binary_sensor: ["on", "off"],
@@ -213,6 +214,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
"pm1",
"pm10",
"pm25",
"pm4",
"power_factor",
"power",
"pressure",
+1
View File
@@ -40,6 +40,7 @@ const STATE_COLORED_DOMAIN = new Set([
"vacuum",
"valve",
"water_heater",
"weather",
]);
export const stateColorCss = (stateObj: HassEntity, state?: string) => {
+2
View File
@@ -32,6 +32,8 @@ export const numberFormatToLocale = (
return ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
case NumberFormat.space_comma:
return ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
case NumberFormat.quote_decimal:
return ["de-CH"]; // Use German (Switzerland) formatting 1'234'567.89
case NumberFormat.system:
return undefined;
default:
+1 -4
View File
@@ -67,10 +67,7 @@ function isSeparatorAtPos(value: string, index: number): boolean {
case undefined:
return false;
default:
if (isEmojiImprecise(code)) {
return true;
}
return false;
return isEmojiImprecise(code);
}
}
+13 -44
View File
@@ -1,13 +1,12 @@
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
import type { FrontendLocaleData } from "../../data/translation";
import type { HomeAssistant } from "../../types";
import {
computeEntityNameDisplay,
type EntityNameItem,
type EntityNameOptions,
} from "../entity/compute_entity_name_display";
import type { LocalizeFunc } from "./localize";
import { computeEntityName } from "../entity/compute_entity_name";
import { computeDeviceName } from "../entity/compute_device_name";
import { getEntityContext } from "../entity/context/get_entity_context";
import { computeAreaName } from "../entity/compute_area_name";
import { computeFloorName } from "../entity/compute_floor_name";
import { ensureArray } from "../array/ensure-array";
export type FormatEntityStateFunc = (
stateObj: HassEntity,
@@ -27,8 +26,8 @@ export type EntityNameType = "entity" | "device" | "area" | "floor";
export type FormatEntityNameFunc = (
stateObj: HassEntity,
type: EntityNameType | EntityNameType[],
separator?: string
name: EntityNameItem | EntityNameItem[],
options?: EntityNameOptions
) => string;
export const computeFormatFunctions = async (
@@ -75,45 +74,15 @@ export const computeFormatFunctions = async (
),
formatEntityAttributeName: (stateObj, attribute) =>
computeAttributeNameDisplay(localize, stateObj, entities, attribute),
formatEntityName: (stateObj, type, separator = " ") => {
const types = ensureArray(type);
const namesList: (string | undefined)[] = [];
const { device, area, floor } = getEntityContext(
formatEntityName: (stateObj, name, options) =>
computeEntityNameDisplay(
stateObj,
name,
entities,
devices,
areas,
floors
);
for (const t of types) {
switch (t) {
case "entity": {
namesList.push(computeEntityName(stateObj, entities, devices));
break;
}
case "device": {
if (device) {
namesList.push(computeDeviceName(device));
}
break;
}
case "area": {
if (area) {
namesList.push(computeAreaName(area));
}
break;
}
case "floor": {
if (floor) {
namesList.push(computeFloorName(floor));
}
break;
}
}
}
return namesList.filter((name) => name !== undefined).join(separator);
},
floors,
options
),
};
};
+1 -1
View File
@@ -14,7 +14,7 @@ export default function parseAspectRatio(input: string) {
}
try {
if (input.endsWith("%")) {
return { w: 100, h: parseOrThrow(input.substr(0, input.length - 1)) };
return { w: 100, h: parseOrThrow(input.slice(0, -1)) };
}
const arr = input.replace(":", "x").split("x");
+116
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;
}
}
+8
View File
@@ -0,0 +1,8 @@
import xss from "xss";
export const filterXSS = (html: string) =>
xss(html, {
whiteList: {},
stripIgnoreTag: true,
stripIgnoreTagBody: true,
});
@@ -6,6 +6,7 @@ import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import "./ha-progress-button";
import type { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import type { Appearance } from "../ha-button";
@customElement("ha-call-service-button")
class HaCallServiceButton extends LitElement {
@@ -25,12 +26,14 @@ class HaCallServiceButton extends LitElement {
@property() public confirmation?;
@property() public appearance: Appearance = "plain";
public render(): TemplateResult {
return html`
<ha-progress-button
.progress=${this.progress}
.disabled=${this.disabled}
appearance="plain"
.appearance=${this.appearance}
@click=${this._buttonTapped}
tabindex="0"
>
+11 -10
View File
@@ -1,21 +1,22 @@
import type { LineSeriesOption } from "echarts";
export function downSampleLineData(
data: LineSeriesOption["data"],
chartWidth: number,
export function downSampleLineData<
T extends [number, number] | NonNullable<LineSeriesOption["data"]>[number],
>(
data: T[] | undefined,
maxDetails: number,
minX?: number,
maxX?: number
) {
if (!data || data.length < 10) {
return data;
): T[] {
if (!data) {
return [];
}
const width = chartWidth * window.devicePixelRatio;
if (data.length <= width) {
if (data.length <= maxDetails) {
return data;
}
const min = minX ?? getPointData(data[0]!)[0];
const max = maxX ?? getPointData(data[data.length - 1]!)[0];
const step = Math.floor((max - min) / width);
const step = Math.ceil((max - min) / Math.floor(maxDetails));
const frames = new Map<
number,
{
@@ -47,7 +48,7 @@ export function downSampleLineData(
}
// Convert frames back to points
const result: typeof data = [];
const result: T[] = [];
for (const [_i, frame] of frames) {
// Use min/max points to preserve visual accuracy
// The order of the data must be preserved so max may be before min
+76 -13
View File
@@ -1,5 +1,5 @@
import { consume } from "@lit/context";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { consume } from "@lit/context";
import { mdiChevronDown, mdiChevronUp, mdiRestart } from "@mdi/js";
import { differenceInMinutes } from "date-fns";
import type { DataZoomComponentOption } from "echarts/components";
@@ -7,27 +7,28 @@ import type { EChartsType } from "echarts/core";
import type {
ECElementEvent,
LegendComponentOption,
LineSeriesOption,
XAXisOption,
YAXisOption,
LineSeriesOption,
} from "echarts/types/dist/shared";
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 { styleMap } from "lit/directives/style-map";
import { ensureArray } from "../../common/array/ensure-array";
import { getAllGraphColors } from "../../common/color/colors";
import { fireEvent } from "../../common/dom/fire_event";
import { listenMediaQuery } from "../../common/dom/media_query";
import { themesContext } from "../../data/context";
import type { Themes } from "../../data/ws-themes";
import type { ECOption } from "../../resources/echarts";
import type { ECOption } from "../../resources/echarts/echarts";
import type { HomeAssistant } from "../../types";
import { isMac } from "../../util/is_mac";
import "../ha-icon-button";
import { formatTimeLabel } from "./axis-label";
import { ensureArray } from "../../common/array/ensure-array";
import "../chips/ha-assist-chip";
import "../ha-icon-button";
import { filterXSS } from "../../common/util/xss";
import { formatTimeLabel } from "./axis-label";
import { downSampleLineData } from "./down-sample";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
@@ -87,9 +88,21 @@ export class HaChartBase extends LitElement {
private _lastTapTime?: number;
private _shouldResizeChart = false;
private _resizeAnimationDuration?: number;
// @ts-ignore
private _resizeController = new ResizeController(this, {
callback: () => this.chart?.resize(),
callback: () => {
if (this.chart) {
if (!this.chart.getZr().animation.isFinished()) {
this._shouldResizeChart = true;
} else {
this.chart.resize();
}
}
},
});
private _loading = false;
@@ -194,6 +207,16 @@ 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;
this._resizeAnimationDuration = 250;
}
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
chartOptions.dataZoom = this._getDataZoomConfig();
}
@@ -285,7 +308,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;
@@ -345,7 +368,7 @@ export class HaChartBase extends LitElement {
if (this.chart) {
this.chart.dispose();
}
const echarts = (await import("../../resources/echarts")).default;
const echarts = (await import("../../resources/echarts/echarts")).default;
if (this.extraComponents?.length) {
echarts.use(this.extraComponents);
@@ -365,6 +388,7 @@ export class HaChartBase extends LitElement {
if (!this.options?.dataZoom) {
this.chart.getZr().on("dblclick", this._handleClickZoom);
}
this.chart.on("finished", this._handleChartRenderFinished);
if (this._isTouchDevice) {
this.chart.getZr().on("click", (e: ECElementEvent) => {
if (!e.zrByTouch) {
@@ -496,6 +520,7 @@ export class HaChartBase extends LitElement {
);
}
});
this.requestUpdate("_hiddenDatasets");
}
private _getDataZoomConfig(): DataZoomComponentOption | undefined {
@@ -629,6 +654,13 @@ export class HaChartBase extends LitElement {
textBorderWidth: 2,
},
},
pie: {
label: {
color: style.getPropertyValue("--primary-text-color"),
textBorderColor: style.getPropertyValue("--primary-background-color"),
textBorderWidth: 2,
},
},
sankey: {
label: {
color: style.getPropertyValue("--primary-text-color"),
@@ -804,14 +836,15 @@ export class HaChartBase extends LitElement {
sampling: undefined,
data: downSampleLineData(
data as LineSeriesOption["data"],
this.clientWidth,
this.clientWidth * window.devicePixelRatio,
minX,
maxX
),
};
}
}
return { ...s, data };
const name = filterXSS(String(s.name ?? s.id ?? ""));
return { ...s, name, data };
});
return series as ECOption["series"];
}
@@ -943,6 +976,36 @@ export class HaChartBase extends LitElement {
});
}
private _handleChartRenderFinished = () => {
if (this._shouldResizeChart) {
this.chart?.resize({
animation:
this._reducedMotion ||
typeof this._resizeAnimationDuration !== "number"
? undefined
: { duration: this._resizeAnimationDuration },
});
this._shouldResizeChart = false;
this._resizeAnimationDuration = undefined;
}
};
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;
@@ -983,7 +1046,7 @@ export class HaChartBase extends LitElement {
.chart-controls ha-icon-button,
.chart-controls ::slotted(ha-icon-button) {
background: var(--card-background-color);
border-radius: 4px;
border-radius: var(--ha-border-radius-sm);
--mdc-icon-button-size: 32px;
color: var(--primary-color);
border: 1px solid var(--divider-color);
@@ -1036,7 +1099,7 @@ export class HaChartBase extends LitElement {
.chart-legend .bullet {
border-width: 1px;
border-style: solid;
border-radius: 50%;
border-radius: var(--ha-border-radius-circle);
display: block;
height: 16px;
width: 16px;
+1 -1
View File
@@ -6,7 +6,7 @@ import type { TopLevelFormatterParams } from "echarts/types/dist/shared";
import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
import memoizeOne from "memoize-one";
import { listenMediaQuery } from "../../common/dom/media_query";
import type { ECOption } from "../../resources/echarts";
import type { ECOption } from "../../resources/echarts/echarts";
import "./ha-chart-base";
import type { HaChartBase } from "./ha-chart-base";
import type { HomeAssistant } from "../../types";
+8 -6
View File
@@ -1,14 +1,15 @@
import { customElement, property, state } from "lit/decorators";
import { LitElement, html, css } from "lit";
import type { EChartsType } from "echarts/core";
import type { CallbackDataParams } from "echarts/types/dist/shared";
import type { SankeySeriesOption } from "echarts/types/dist/echarts";
import { SankeyChart } from "echarts/charts";
import type { CallbackDataParams } from "echarts/types/src/util/types";
import memoizeOne from "memoize-one";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import SankeyChart from "../../resources/echarts/components/sankey/install";
import type { HomeAssistant } from "../../types";
import type { ECOption } from "../../resources/echarts";
import type { ECOption } from "../../resources/echarts/echarts";
import { measureTextWidth } from "../../util/text";
import { filterXSS } from "../../common/util/xss";
import "./ha-chart-base";
import { NODE_SIZE } from "../trace/hat-graph-const";
import "../ha-alert";
@@ -38,7 +39,7 @@ type ProcessedLink = Link & {
const OVERFLOW_MARGIN = 5;
const FONT_SIZE = 12;
const NODE_GAP = 8;
const NODE_GAP = 6;
const LABEL_DISTANCE = 5;
@customElement("ha-sankey-chart")
@@ -92,12 +93,12 @@ export class HaSankeyChart extends LitElement {
: data.value;
if (data.id) {
const node = this.data.nodes.find((n) => n.id === data.id);
return `${params.marker} ${node?.label ?? data.id}<br>${value}`;
return `${params.marker} ${filterXSS(node?.label ?? data.id)}<br>${value}`;
}
if (data.source && data.target) {
const source = this.data.nodes.find((n) => n.id === data.source);
const target = this.data.nodes.find((n) => n.id === data.target);
return `${source?.label ?? data.source}${target?.label ?? data.target}<br>${value}`;
return `${filterXSS(source?.label ?? data.source)}${filterXSS(target?.label ?? data.target)}<br>${value}`;
}
return null;
};
@@ -163,6 +164,7 @@ export class HaSankeyChart extends LitElement {
lineStyle: {
color: "gradient",
opacity: 0.4,
curveness: 0.5,
},
layoutIterations: 0,
label: {
@@ -11,7 +11,7 @@ import { computeRTL } from "../../common/util/compute_rtl";
import type { LineChartEntity, LineChartState } from "../../data/history";
import type { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import type { ECOption } from "../../resources/echarts";
import type { ECOption } from "../../resources/echarts/echarts";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import {
getNumberFormatOptions,
@@ -87,6 +87,8 @@ export class StateHistoryChartLine extends LitElement {
private _previousYAxisLabelValue = 0;
private _yAxisMaximumFractionDigits = 0;
protected render() {
return html`
<ha-chart-base
@@ -757,8 +759,12 @@ export class StateHistoryChartLine extends LitElement {
Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1))
)
);
this._yAxisMaximumFractionDigits = Math.max(
this._yAxisMaximumFractionDigits,
maximumFractionDigits
);
const label = formatNumber(value, this.hass.locale, {
maximumFractionDigits,
maximumFractionDigits: this._yAxisMaximumFractionDigits,
});
const width = measureTextWidth(label, 12) + 5;
if (width > this._yWidth) {
@@ -15,8 +15,8 @@ import type { TimelineEntity } from "../../data/history";
import type { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import { computeTimelineColor } from "./timeline-color";
import type { ECOption } from "../../resources/echarts";
import echarts from "../../resources/echarts";
import type { ECOption } from "../../resources/echarts/echarts";
import echarts from "../../resources/echarts/echarts";
import { luminosity } from "../../common/color/rgb";
import { hex2rgb } from "../../common/color/convert-color";
import { measureTextWidth } from "../../util/text";
+1 -1
View File
@@ -29,7 +29,7 @@ import {
getStatisticMetadata,
statisticsHaveType,
} from "../../data/recorder";
import type { ECOption } from "../../resources/echarts";
import type { ECOption } from "../../resources/echarts/echarts";
import type { HomeAssistant } from "../../types";
import type { CustomLegendOption } from "./ha-chart-base";
import "./ha-chart-base";
+25
View File
@@ -6,6 +6,8 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { stateColorProperties } from "../../common/entity/state_color";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { computeCssValue } from "../../resources/css-variables";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
media_player: {
@@ -51,6 +53,28 @@ function computeTimelineStateColor(
let colorIndex = 0;
const stateColorMap = new Map<string, string>();
function computeTimelineEnumColor(
state: string,
computedStyles: CSSStyleDeclaration,
stateObj?: HassEntity
): string | undefined {
if (!stateObj) {
return undefined;
}
const domain = computeStateDomain(stateObj);
const states =
FIXED_DOMAIN_STATES[domain] ||
(domain === "sensor" &&
stateObj.attributes.device_class === "enum" &&
stateObj.attributes.options) ||
[];
const idx = states.indexOf(state);
if (idx === -1) {
return undefined;
}
return getGraphColorByIndex(idx, computedStyles);
}
function computeTimeLineGenericColor(
state: string,
computedStyles: CSSStyleDeclaration
@@ -71,6 +95,7 @@ export function computeTimelineColor(
): string {
return (
computeTimelineStateColor(state, computedStyles, stateObj) ||
computeTimelineEnumColor(state, computedStyles, stateObj) ||
computeTimeLineGenericColor(state, computedStyles)
);
}
+2 -1
View File
@@ -1,9 +1,9 @@
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles";
import { FilterChip } from "@material/web/chips/internal/filter-chip";
import { styles } from "@material/web/chips/internal/filter-styles";
import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles";
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles";
import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles";
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles";
import { css, html } from "lit";
import { customElement, property } from "lit/decorators";
@@ -30,6 +30,7 @@ export class HaFilterChip extends FilterChip {
var(--rgb-primary-text-color),
0.15
);
border-radius: var(--ha-border-radius-md);
}
`,
];
@@ -1,4 +1,4 @@
import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js";
import { mdiDragHorizontalVariant, mdiEye, mdiEyeOff } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -129,7 +129,7 @@ export class DialogDataTableSettings extends LitElement {
${canMove && isVisible
? html`<ha-svg-icon
class="handle"
.path=${mdiDrag}
.path=${mdiDragHorizontalVariant}
slot="graphic"
></ha-svg-icon>`
: nothing}
@@ -290,7 +290,9 @@ export class DialogDataTableSettings extends LitElement {
ha-dialog {
--vertical-align-dialog: flex-start;
--dialog-surface-margin-top: 250px;
--ha-dialog-border-radius: 28px 28px 0 0;
--ha-dialog-border-radius: var(--ha-border-radius-4xl)
var(--ha-border-radius-4xl) var(--ha-border-radius-square)
var(--ha-border-radius-square);
--mdc-dialog-min-height: calc(100% - 250px);
--mdc-dialog-max-height: calc(100% - 250px);
}
+1 -1
View File
@@ -1053,7 +1053,7 @@ export class HaDataTable extends LitElement {
.mdc-data-table {
background-color: var(--data-table-background-color);
border-radius: 4px;
border-radius: var(--ha-border-radius-sm);
border-width: 1px;
border-style: solid;
border-color: var(--divider-color);
+38 -16
View File
@@ -1,6 +1,9 @@
import { expose } from "comlink";
import { stringCompare, ipCompare } from "../../common/string/compare";
import Fuse from "fuse.js";
import memoizeOne from "memoize-one";
import { ipCompare, stringCompare } from "../../common/string/compare";
import { stripDiacritics } from "../../common/string/strip-diacritics";
import { HaFuse } from "../../resources/fuse";
import type {
ClonedDataTableColumnData,
DataTableRowData,
@@ -8,29 +11,48 @@ import type {
SortingDirection,
} from "./ha-data-table";
const fuseIndex = memoizeOne(
(data: DataTableRowData[], columns: SortableColumnContainer) => {
const searchKeys = new Set<string>();
Object.entries(columns).forEach(([key, column]) => {
if (column.filterable) {
searchKeys.add(
column.filterKey
? `${column.valueColumn || key}.${column.filterKey}`
: key
);
}
});
return Fuse.createIndex([...searchKeys], data);
}
);
const filterData = (
data: DataTableRowData[],
columns: SortableColumnContainer,
filter: string
) => {
filter = stripDiacritics(filter.toLowerCase());
return data.filter((row) =>
Object.entries(columns).some((columnEntry) => {
const [key, column] = columnEntry;
if (column.filterable) {
const value = String(
column.filterKey
? row[column.valueColumn || key][column.filterKey]
: row[column.valueColumn || key]
);
if (stripDiacritics(value).toLowerCase().includes(filter)) {
return true;
}
}
return false;
})
if (filter === "") {
return data;
}
const index = fuseIndex(data, columns);
const fuse = new HaFuse(
data,
{ shouldSort: false, minMatchCharLength: 1 },
index
);
const searchResults = fuse.multiTermsSearch(filter);
if (searchResults) {
return searchResults.map((result) => result.item);
}
return data;
};
const sortData = (
+10 -10
View File
@@ -4,11 +4,11 @@ import Vue from "vue";
import DateRangePicker from "vue2-daterange-picker";
// @ts-ignore
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
import { fireEvent } from "../common/dom/fire_event";
import {
localizeWeekdays,
localizeMonths,
localizeWeekdays,
} from "../common/datetime/localize_date";
import { fireEvent } from "../common/dom/fire_event";
import { mainWindow } from "../common/dom/get_main_window";
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -177,7 +177,7 @@ class DateRangePickerElement extends WrappedElement {
top: auto;
box-shadow: var(--ha-card-box-shadow, none);
background-color: var(--card-background-color);
border-radius: var(--ha-card-border-radius, 12px);
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
border-width: var(--ha-card-border-width, 1px);
border-style: solid;
border-color: var(
@@ -203,7 +203,7 @@ class DateRangePickerElement extends WrappedElement {
.daterangepicker .calendar-table th {
background-color: transparent;
color: var(--secondary-text-color);
border-radius: 0;
border-radius: var(--ha-border-radius-square);
outline: none;
min-width: 32px;
height: 32px;
@@ -225,13 +225,13 @@ class DateRangePickerElement extends WrappedElement {
color: var(--text-primary-color);
}
.daterangepicker td.start-date.end-date {
border-radius: 50%;
border-radius: var(--ha-border-radius-circle);
}
.daterangepicker td.start-date {
border-radius: 50% 0 0 50%;
border-radius: var(--ha-border-radius-circle) var(--ha-border-radius-square) var(--ha-border-radius-square) var(--ha-border-radius-circle);
}
.daterangepicker td.end-date {
border-radius: 0 50% 50% 0;
border-radius: var(--ha-border-radius-square) var(--ha-border-radius-circle) var(--ha-border-radius-circle) var(--ha-border-radius-square);
}
.reportrange-text {
background: none !important;
@@ -265,7 +265,7 @@ class DateRangePickerElement extends WrappedElement {
border: 1px solid var(--primary-color);
background-color: transparent;
color: var(--primary-color);
border-radius: 4px;
border-radius: var(--ha-border-radius-sm);
padding: 8px;
cursor: pointer;
}
@@ -321,10 +321,10 @@ class DateRangePickerElement extends WrappedElement {
-webkit-transform: rotate(-45deg);
}
.daterangepicker td.start-date {
border-radius: 0 50% 50% 0;
border-radius: var(--ha-border-radius-square) var(--ha-border-radius-circle) var(--ha-border-radius-circle) var(--ha-border-radius-square);
}
.daterangepicker td.end-date {
border-radius: 50% 0 0 50%;
border-radius: var(--ha-border-radius-circle) var(--ha-border-radius-square) var(--ha-border-radius-square) var(--ha-border-radius-circle);
}
`;
}
+9 -162
View File
@@ -5,24 +5,18 @@ import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeAreaName } from "../../common/entity/compute_area_name";
import {
computeDeviceName,
computeDeviceNameDisplay,
} from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { getDeviceContext } from "../../common/entity/context/get_device_context";
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
import {
getDeviceEntityDisplayLookup,
type DeviceEntityDisplayLookup,
getDevices,
type DevicePickerItem,
type DeviceRegistryEntry,
} from "../../data/device_registry";
import { domainToName } from "../../data/integration";
import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import "../ha-generic-picker";
import type { HaGenericPicker } from "../ha-generic-picker";
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry
@@ -30,11 +24,6 @@ export type HaDevicePickerDeviceFilterFunc = (
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
interface DevicePickerItem extends PickerComboBoxItem {
domain?: string;
domain_name?: string;
}
@customElement("ha-device-picker")
export class HaDevicePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -104,6 +93,8 @@ export class HaDevicePicker extends LitElement {
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
private _getDevicesMemoized = memoizeOne(getDevices);
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
this._loadConfigEntries();
@@ -117,162 +108,18 @@ export class HaDevicePicker extends LitElement {
}
private _getItems = () =>
this._getDevices(
this.hass.devices,
this.hass.entities,
this._getDevicesMemoized(
this.hass,
this._configEntryLookup,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.excludeDevices
this.excludeDevices,
this.value
);
private _getDevices = memoizeOne(
(
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
configEntryLookup: Record<string, ConfigEntry>,
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"],
entityFilter: this["entityFilter"],
excludeDevices: this["excludeDevices"]
): DevicePickerItem[] => {
const devices = Object.values(haDevices);
const entities = Object.values(haEntities);
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
if (
includeDomains ||
excludeDomains ||
includeDeviceClasses ||
entityFilter
) {
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
}
let inputDevices = devices.filter(
(device) => device.id === this.value || !device.disabled_by
);
if (includeDomains) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
}
if (excludeDomains) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
});
}
if (excludeDevices) {
inputDevices = inputDevices.filter(
(device) => !excludeDevices!.includes(device.id)
);
}
if (includeDeviceClasses) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
}
if (entityFilter) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return devEntities.some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter(stateObj);
});
});
}
if (deviceFilter) {
inputDevices = inputDevices.filter(
(device) =>
// We always want to include the device of the current value
device.id === this.value || deviceFilter!(device)
);
}
const outputDevices = inputDevices.map<DevicePickerItem>((device) => {
const deviceName = computeDeviceNameDisplay(
device,
this.hass,
deviceEntityLookup[device.id]
);
const { area } = getDeviceContext(device, this.hass);
const areaName = area ? computeAreaName(area) : undefined;
const configEntry = device.primary_config_entry
? configEntryLookup?.[device.primary_config_entry]
: undefined;
const domain = configEntry?.domain;
const domainName = domain
? domainToName(this.hass.localize, domain)
: undefined;
return {
id: device.id,
label: "",
primary:
deviceName ||
this.hass.localize("ui.components.device-picker.unnamed_device"),
secondary: areaName,
domain: configEntry?.domain,
domain_name: domainName,
search_labels: [deviceName, areaName, domain, domainName].filter(
Boolean
) as string[],
sorting_label: deviceName || "zzz",
};
});
return outputDevices;
}
);
private _valueRenderer = memoizeOne(
(configEntriesLookup: Record<string, ConfigEntry>) => (value: string) => {
const deviceId = value;
+1
View File
@@ -0,0 +1 @@
export const ANY_STATE_VALUE = "__ANY_STATE_IGNORE_ATTRIBUTES__";
+4 -3
View File
@@ -1,13 +1,13 @@
import { mdiDrag } from "@mdi/js";
import { mdiDragHorizontalVariant } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-sortable";
import "./ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
@customElement("ha-entities-picker")
class HaEntitiesPicker extends LitElement {
@@ -118,7 +118,7 @@ class HaEntitiesPicker extends LitElement {
? html`
<ha-svg-icon
class="entity-handle"
.path=${mdiDrag}
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
`
: nothing}
@@ -147,6 +147,7 @@ class HaEntitiesPicker extends LitElement {
.createDomains=${this.createDomains}
.required=${this.required && !currentEntities.length}
@value-changed=${this._addEntity}
.addButton=${currentEntities.length > 0}
></ha-entity-picker>
</div>
`;
@@ -1,4 +1,3 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@@ -8,8 +7,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
interface AttributeOption {
value: string;
label: string;
@@ -0,0 +1,536 @@
import "@material/mwc-menu/mwc-menu-surface";
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import type { EntityNameItem } from "../../common/entity/compute_entity_name_display";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import type { EntityNameType } from "../../common/translations/entity-state";
import type { LocalizeKeys } from "../../common/translations/localize";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../chips/ha-assist-chip";
import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-input-helper-text";
import "../ha-sortable";
interface EntityNameOption {
primary: string;
secondary?: string;
field_label: string;
value: string;
}
const rowRenderer: ComboBoxLitRenderer<EntityNameOption> = (item) => html`
<ha-combo-box-item type="button">
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
</ha-combo-box-item>
`;
const KNOWN_TYPES = new Set(["entity", "device", "area", "floor"]);
const UNIQUE_TYPES = new Set(["entity", "device", "area", "floor"]);
const formatOptionValue = (item: EntityNameItem) => {
if (item.type === "text" && item.text) {
return item.text;
}
return `___${item.type}___`;
};
const parseOptionValue = (value: string): EntityNameItem => {
if (value.startsWith("___") && value.endsWith("___")) {
const type = value.slice(3, -3);
if (KNOWN_TYPES.has(type)) {
return { type: type as EntityNameType };
}
}
return { type: "text", text: value };
};
@customElement("ha-entity-name-picker")
export class HaEntityNamePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId?: string;
@property({ attribute: false }) public value?:
| string
| EntityNameItem
| EntityNameItem[];
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, reflect: true }) public disabled = false;
@query(".container", true) private _container?: HTMLDivElement;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
@state() private _opened = false;
private _editIndex?: number;
private _validTypes = memoizeOne((entityId?: string) => {
const options = new Set<string>(["text"]);
if (!entityId) {
return options;
}
const stateObj = this.hass.states[entityId];
if (!stateObj) {
return options;
}
options.add("entity");
const context = getEntityContext(
stateObj,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
if (context.device) options.add("device");
if (context.area) options.add("area");
if (context.floor) options.add("floor");
return options;
});
private _getOptions = memoizeOne((entityId?: string) => {
if (!entityId) {
return [];
}
const types = this._validTypes(entityId);
const items = (
["entity", "device", "area", "floor"] as const
).map<EntityNameOption>((name) => {
const stateObj = this.hass.states[entityId];
const isValid = types.has(name);
const primary = this.hass.localize(
`ui.components.entity.entity-name-picker.types.${name}`
);
const secondary =
(stateObj && isValid
? this.hass.formatEntityName(stateObj, { type: name })
: this.hass.localize(
`ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys
)) || "-";
return {
primary,
secondary,
field_label: primary,
value: formatOptionValue({ type: name }),
};
});
return items;
});
private _customNameOption = memoizeOne((text: string) => ({
primary: this.hass.localize(
"ui.components.entity.entity-name-picker.custom_name"
),
secondary: `"${text}"`,
field_label: text,
value: formatOptionValue({ type: "text", text }),
}));
private _formatItem = (item: EntityNameItem) => {
if (item.type === "text") {
return `"${item.text}"`;
}
if (KNOWN_TYPES.has(item.type)) {
return this.hass.localize(
`ui.components.entity.entity-name-picker.types.${item.type as EntityNameType}`
);
}
return item.type;
};
protected render() {
const value = this._items;
const options = this._getOptions(this.entityId);
const validTypes = this._validTypes(this.entityId);
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<div class="container">
<ha-sortable
no-style
@item-moved=${this._moveItem}
.disabled=${this.disabled}
handle-selector="button.primary.action"
filter=".add"
>
<ha-chip-set>
${repeat(
this._items,
(item) => item,
(item: EntityNameItem, idx) => {
const label = this._formatItem(item);
const isValid = validTypes.has(item.type);
return html`
<ha-input-chip
data-idx=${idx}
@remove=${this._removeItem}
@click=${this._editItem}
.label=${label}
.selected=${!this.disabled}
.disabled=${this.disabled}
class=${!isValid ? "invalid" : ""}
>
<ha-svg-icon
slot="icon"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
<span>${label}</span>
</ha-input-chip>
`;
}
)}
${this.disabled
? nothing
: html`
<ha-assist-chip
@click=${this._addItem}
.disabled=${this.disabled}
label=${this.hass.localize(
"ui.components.entity.entity-name-picker.add"
)}
class="add"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-assist-chip>
`}
</ha-chip-set>
</ha-sortable>
<mwc-menu-surface
.open=${this._opened}
@closed=${this._onClosed}
@opened=${this._onOpened}
@input=${stopPropagation}
.anchor=${this._container}
>
<ha-combo-box
.hass=${this.hass}
.value=${""}
.autofocus=${this.autofocus}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.items=${options}
allow-custom-value
item-id-path="value"
item-value-path="value"
item-label-path="field_label"
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._comboBoxValueChanged}
@filter-changed=${this._filterChanged}
>
</ha-combo-box>
</mwc-menu-surface>
</div>
${this._renderHelper()}
`;
}
private _renderHelper() {
return this.helper
? html`
<ha-input-helper-text .disabled=${this.disabled}>
${this.helper}
</ha-input-helper-text>
`
: nothing;
}
private _onClosed(ev) {
ev.stopPropagation();
this._opened = false;
this._editIndex = undefined;
}
private async _onOpened(ev) {
if (!this._opened) {
return;
}
ev.stopPropagation();
this._opened = true;
await this._comboBox?.focus();
await this._comboBox?.open();
}
private async _addItem(ev) {
ev.stopPropagation();
this._opened = true;
}
private async _editItem(ev) {
ev.stopPropagation();
const idx = parseInt(ev.currentTarget.dataset.idx, 10);
this._editIndex = idx;
this._opened = true;
}
private get _items(): EntityNameItem[] {
return this._toItems(this.value);
}
private _toItems = memoizeOne((value?: typeof this.value) => {
if (typeof value === "string") {
if (value === "") {
return [];
}
return [{ type: "text", text: value } satisfies EntityNameItem];
}
return value ? ensureArray(value) : [];
});
private _toValue = memoizeOne(
(items: EntityNameItem[]): typeof this.value => {
if (items.length === 0) {
return undefined;
}
if (items.length === 1) {
const item = items[0];
return item.type === "text" ? item.text : item;
}
return items;
}
);
private _openedChanged(ev: ValueChangedEvent<boolean>) {
const open = ev.detail.value;
if (open) {
const options = this._comboBox.items || [];
const initialItem =
this._editIndex != null ? this._items[this._editIndex] : undefined;
const initialValue = initialItem ? formatOptionValue(initialItem) : "";
const filteredItems = this._filterSelectedOptions(options, initialValue);
if (initialItem?.type === "text" && initialItem.text) {
filteredItems.push(this._customNameOption(initialItem.text));
}
this._comboBox.filteredItems = filteredItems;
this._comboBox.setInputValue(initialValue);
} else {
this._opened = false;
this._comboBox.setInputValue("");
}
}
private _filterSelectedOptions = (
options: EntityNameOption[],
current?: string
) => {
const items = this._items;
const excludedValues = new Set(
items
.filter((item) => UNIQUE_TYPES.has(item.type))
.map((item) => formatOptionValue(item))
);
const filteredOptions = options.filter(
(option) => !excludedValues.has(option.value) || option.value === current
);
return filteredOptions;
};
private _filterChanged(ev: ValueChangedEvent<string>) {
const input = ev.detail.value;
const filter = input?.toLowerCase() || "";
const options = this._comboBox.items || [];
const currentItem =
this._editIndex != null ? this._items[this._editIndex] : undefined;
const currentValue = currentItem ? formatOptionValue(currentItem) : "";
let filteredItems = this._filterSelectedOptions(options, currentValue);
if (!filter) {
this._comboBox.filteredItems = filteredItems;
return;
}
const fuseOptions: IFuseOptions<EntityNameOption> = {
keys: ["primary", "secondary", "value"],
isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
ignoreDiacritics: true,
};
const fuse = new Fuse(filteredItems, fuseOptions);
filteredItems = fuse.search(filter).map((result) => result.item);
filteredItems.push(this._customNameOption(input));
this._comboBox.filteredItems = filteredItems;
}
private async _moveItem(ev: CustomEvent) {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
const value = this._items;
const newValue = value.concat();
const element = newValue.splice(oldIndex, 1)[0];
newValue.splice(newIndex, 0, element);
this._setValue(newValue);
await this.updateComplete;
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
}
private async _removeItem(ev) {
ev.stopPropagation();
const value = [...this._items];
const idx = parseInt(ev.target.dataset.idx, 10);
value.splice(idx, 1);
this._setValue(value);
await this.updateComplete;
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
}
private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
ev.stopPropagation();
const value = ev.detail.value;
if (this.disabled || value === "") {
return;
}
const item: EntityNameItem = parseOptionValue(value);
const newValue = [...this._items];
if (this._editIndex != null) {
newValue[this._editIndex] = item;
} else {
newValue.push(item);
}
this._setValue(newValue);
}
private _setValue(value: EntityNameItem[]) {
const newValue = this._toValue(value);
this.value = newValue;
fireEvent(this, "value-changed", {
value: newValue,
});
}
static styles = css`
:host {
position: relative;
width: 100%;
}
.container {
position: relative;
background-color: var(--mdc-text-field-fill-color, whitesmoke);
border-radius: var(--ha-border-radius-sm);
border-end-end-radius: var(--ha-border-radius-square);
border-end-start-radius: var(--ha-border-radius-square);
}
.container:after {
display: block;
content: "";
position: absolute;
pointer-events: none;
bottom: 0;
left: 0;
right: 0;
height: 1px;
width: 100%;
background-color: var(
--mdc-text-field-idle-line-color,
rgba(0, 0, 0, 0.42)
);
transform:
height 180ms ease-in-out,
background-color 180ms ease-in-out;
}
:host([disabled]) .container:after {
background-color: var(
--mdc-text-field-disabled-line-color,
rgba(0, 0, 0, 0.42)
);
}
.container:focus-within:after {
height: 2px;
background-color: var(--mdc-theme-primary);
}
label {
display: block;
margin: 0 0 var(--ha-space-2);
}
.add {
order: 1;
}
mwc-menu-surface {
--mdc-menu-min-width: 100%;
}
ha-chip-set {
padding: var(--ha-space-2) var(--ha-space-2);
}
.invalid {
text-decoration: line-through;
}
.sortable-fallback {
display: none;
opacity: 0;
}
.sortable-ghost {
opacity: 0.4;
}
.sortable-drag {
cursor: grabbing;
}
ha-input-helper-text {
display: block;
margin: var(--ha-space-2) 0 0;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-entity-name-picker": HaEntityNamePicker;
}
}
+27 -136
View File
@@ -1,14 +1,17 @@
import { mdiPlus, mdiShape } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import { computeRTL } from "../../common/util/compute_rtl";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import {
getEntities,
type EntityComboBoxItem,
} from "../../data/entity_registry";
import { domainToName } from "../../data/integration";
import {
isHelperDomain,
@@ -19,21 +22,11 @@ import type { HomeAssistant } from "../../types";
import "../ha-combo-box-item";
import "../ha-generic-picker";
import type { HaGenericPicker } from "../ha-generic-picker";
import type {
PickerComboBoxItem,
PickerComboBoxSearchFn,
} from "../ha-picker-combo-box";
import type { PickerComboBoxSearchFn } from "../ha-picker-combo-box";
import type { PickerValueRenderer } from "../ha-picker-field";
import "../ha-svg-icon";
import "./state-badge";
interface EntityComboBoxItem extends PickerComboBoxItem {
domain_name?: string;
stateObj?: HassEntity;
}
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
const CREATE_ID = "___create-new-entity___";
@customElement("ha-entity-picker")
@@ -120,6 +113,9 @@ export class HaEntityPicker extends LitElement {
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ attribute: "add-button", type: Boolean })
public addButton = false;
@query("ha-generic-picker") private _picker?: HaGenericPicker;
protected firstUpdated(changedProperties: PropertyValues): void {
@@ -144,9 +140,14 @@ export class HaEntityPicker extends LitElement {
`;
}
const entityName = this.hass.formatEntityName(stateObj, "entity");
const deviceName = this.hass.formatEntityName(stateObj, "device");
const areaName = this.hass.formatEntityName(stateObj, "area");
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const isRTL = computeRTL(this.hass);
@@ -249,8 +250,10 @@ export class HaEntityPicker extends LitElement {
}
);
private _getEntitiesMemoized = memoizeOne(getEntities);
private _getItems = () =>
this._getEntities(
this._getEntitiesMemoized(
this.hass,
this.includeDomains,
this.excludeDomains,
@@ -258,125 +261,10 @@ export class HaEntityPicker extends LitElement {
this.includeDeviceClasses,
this.includeUnitOfMeasurement,
this.includeEntities,
this.excludeEntities
this.excludeEntities,
this.value
);
private _getEntities = memoizeOne(
(
hass: this["hass"],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
includeEntities: this["includeEntities"],
excludeEntities: this["excludeEntities"]
): EntityComboBoxItem[] => {
let items: EntityComboBoxItem[] = [];
let entityIds = Object.keys(hass.states);
if (includeEntities) {
entityIds = entityIds.filter((entityId) =>
includeEntities.includes(entityId)
);
}
if (excludeEntities) {
entityIds = entityIds.filter(
(entityId) => !excludeEntities.includes(entityId)
);
}
if (includeDomains) {
entityIds = entityIds.filter((eid) =>
includeDomains.includes(computeDomain(eid))
);
}
if (excludeDomains) {
entityIds = entityIds.filter(
(eid) => !excludeDomains.includes(computeDomain(eid))
);
}
const isRTL = computeRTL(this.hass);
items = entityIds.map<EntityComboBoxItem>((entityId) => {
const stateObj = hass!.states[entityId];
const friendlyName = computeStateName(stateObj); // Keep this for search
const entityName = this.hass.formatEntityName(stateObj, "entity");
const deviceName = this.hass.formatEntityName(stateObj, "device");
const areaName = this.hass.formatEntityName(stateObj, "area");
const domainName = domainToName(
this.hass.localize,
computeDomain(entityId)
);
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
return {
id: entityId,
primary: primary,
secondary: secondary,
domain_name: domainName,
sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
search_labels: [
entityName,
deviceName,
areaName,
domainName,
friendlyName,
entityId,
].filter(Boolean) as string[],
a11y_label: a11yLabel,
stateObj: stateObj,
};
});
if (includeDeviceClasses) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj?.attributes.device_class &&
includeDeviceClasses.includes(
item.stateObj.attributes.device_class
))
);
}
if (includeUnitOfMeasurement) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj?.attributes.unit_of_measurement &&
includeUnitOfMeasurement.includes(
item.stateObj.attributes.unit_of_measurement
))
);
}
if (entityFilter) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj && entityFilter!(item.stateObj))
);
}
return items;
}
);
protected render() {
const placeholder =
this.placeholder ??
@@ -396,7 +284,7 @@ export class HaEntityPicker extends LitElement {
.searchLabel=${this.searchLabel}
.notFoundLabel=${notFoundLabel}
.placeholder=${placeholder}
.value=${this.value}
.value=${this.addButton ? undefined : this.value}
.rowRenderer=${this._rowRenderer}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}
@@ -404,6 +292,9 @@ export class HaEntityPicker extends LitElement {
.searchFn=${this._searchFn}
.valueRenderer=${this._valueRenderer}
@value-changed=${this._valueChanged}
.addButtonLabel=${this.addButton
? this.hass.localize("ui.components.entity.entity-picker.add")
: undefined}
>
</ha-generic-picker>
`;
@@ -1,23 +1,39 @@
import { mdiDrag } from "@mdi/js";
import "@material/mwc-menu/mwc-menu-surface";
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeDomain } from "../../common/entity/compute_domain";
import {
STATE_DISPLAY_SPECIAL_CONTENT,
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS,
} from "../../state-display/state-display";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import "../ha-sortable";
import "../chips/ha-input-chip";
import "../chips/ha-assist-chip";
import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-sortable";
interface StateContentOption {
primary: string;
value: string;
}
const rowRenderer: ComboBoxLitRenderer<StateContentOption> = (item) => html`
<ha-combo-box-item type="button">
<span slot="headline">${item.primary}</span>
</ha-combo-box-item>
`;
const HIDDEN_ATTRIBUTES = [
"access_token",
@@ -74,7 +90,7 @@ const HIDDEN_ATTRIBUTES = [
];
@customElement("ha-entity-state-content-picker")
class HaEntityStatePicker extends LitElement {
export class HaStateContentPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId?: string;
@@ -95,26 +111,28 @@ class HaEntityStatePicker extends LitElement {
@property() public helper?: string;
@state() private _opened = false;
@query(".container", true) private _container?: HTMLDivElement;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
protected shouldUpdate(changedProps: PropertyValues) {
return !(!changedProps.has("_opened") && this._opened);
}
@state() private _opened = false;
private options = memoizeOne(
private _editIndex?: number;
private _options = memoizeOne(
(entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
const domain = entityId ? computeDomain(entityId) : undefined;
return [
{
label: this.hass.localize("ui.components.state-content-picker.state"),
primary: this.hass.localize(
"ui.components.state-content-picker.state"
),
value: "state",
},
...(allowName
? [
{
label: this.hass.localize(
primary: this.hass.localize(
"ui.components.state-content-picker.name"
),
value: "name",
@@ -122,13 +140,13 @@ class HaEntityStatePicker extends LitElement {
]
: []),
{
label: this.hass.localize(
primary: this.hass.localize(
"ui.components.state-content-picker.last_changed"
),
value: "last_changed",
},
{
label: this.hass.localize(
primary: this.hass.localize(
"ui.components.state-content-picker.last_updated"
),
value: "last_updated",
@@ -137,7 +155,7 @@ class HaEntityStatePicker extends LitElement {
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
).map((content) => ({
label: this.hass.localize(
primary: this.hass.localize(
`ui.components.state-content-picker.${content}`
),
value: content,
@@ -146,105 +164,201 @@ class HaEntityStatePicker extends LitElement {
...Object.keys(stateObj?.attributes ?? {})
.filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
.map((attribute) => ({
primary: this.hass.formatEntityAttributeName(stateObj!, attribute),
value: attribute,
label: this.hass.formatEntityAttributeName(stateObj!, attribute),
})),
];
] satisfies StateContentOption[];
}
);
private _filter = "";
protected render() {
if (!this.hass) {
return nothing;
}
const value = this._value;
const stateObj = this.entityId
? this.hass.states[this.entityId]
: undefined;
const options = this.options(this.entityId, stateObj, this.allowName);
const optionItems = options.filter(
(option) => !this._value.includes(option.value)
);
const options = this._options(this.entityId, stateObj, this.allowName);
return html`
${value?.length
? html`
<ha-sortable
no-style
@item-moved=${this._moveItem}
.disabled=${this.disabled}
handle-selector="button.primary.action"
>
<ha-chip-set>
${repeat(
this._value,
(item) => item,
(item, idx) => {
const label =
options.find((option) => option.value === item)?.label ||
item;
return html`
<ha-input-chip
.idx=${idx}
@remove=${this._removeItem}
.label=${label}
selected
>
<ha-svg-icon slot="icon" .path=${mdiDrag}></ha-svg-icon>
${label}
</ha-input-chip>
`;
}
)}
</ha-chip-set>
</ha-sortable>
`
: nothing}
${this.label ? html`<label>${this.label}</label>` : nothing}
<div class="container ${this.disabled ? "disabled" : ""}">
<ha-sortable
no-style
@item-moved=${this._moveItem}
.disabled=${this.disabled}
handle-selector="button.primary.action"
filter=".add"
>
<ha-chip-set>
${repeat(
this._value,
(item) => item,
(item: string, idx) => {
const label = options.find((o) => o.value === item)?.primary;
const isValid = !!label;
return html`
<ha-input-chip
data-idx=${idx}
@remove=${this._removeItem}
@click=${this._editItem}
.label=${label || item}
.selected=${!this.disabled}
.disabled=${this.disabled}
class=${!isValid ? "invalid" : ""}
>
<ha-svg-icon
slot="icon"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</ha-input-chip>
`;
}
)}
${this.disabled
? nothing
: html`
<ha-assist-chip
@click=${this._addItem}
.disabled=${this.disabled}
label=${this.hass.localize(
"ui.components.entity.entity-state-content-picker.add"
)}
class="add"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-assist-chip>
`}
</ha-chip-set>
</ha-sortable>
<ha-combo-box
item-value-path="value"
item-label-path="label"
.hass=${this.hass}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.value=${""}
.items=${optionItems}
allow-custom-value
@filter-changed=${this._filterChanged}
@value-changed=${this._comboBoxValueChanged}
@opened-changed=${this._openedChanged}
></ha-combo-box>
<mwc-menu-surface
.open=${this._opened}
@closed=${this._onClosed}
@opened=${this._onOpened}
@input=${stopPropagation}
.anchor=${this._container}
>
<ha-combo-box
.hass=${this.hass}
.value=${""}
.autofocus=${this.autofocus}
.disabled=${this.disabled || !this.entityId}
.required=${this.required && !value.length}
.helper=${this.helper}
.items=${options}
allow-custom-value
item-id-path="value"
item-value-path="value"
item-label-path="primary"
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._comboBoxValueChanged}
@filter-changed=${this._filterChanged}
>
</ha-combo-box>
</mwc-menu-surface>
</div>
`;
}
private _onClosed(ev) {
ev.stopPropagation();
this._opened = false;
this._editIndex = undefined;
}
private async _onOpened(ev) {
if (!this._opened) {
return;
}
ev.stopPropagation();
this._opened = true;
await this._comboBox?.focus();
await this._comboBox?.open();
}
private async _addItem(ev) {
ev.stopPropagation();
this._opened = true;
}
private async _editItem(ev) {
ev.stopPropagation();
const idx = parseInt(ev.currentTarget.dataset.idx, 10);
this._editIndex = idx;
this._opened = true;
}
private get _value() {
return !this.value ? [] : ensureArray(this.value);
}
private _toValue = memoizeOne((value: string[]): typeof this.value => {
if (value.length === 0) {
return undefined;
}
if (value.length === 1) {
return value[0];
}
return value;
});
private _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
this._comboBox.filteredItems = this._comboBox.items;
const open = ev.detail.value;
if (open) {
const options = this._comboBox.items || [];
const initialValue =
this._editIndex != null ? this._value[this._editIndex] : "";
const filteredItems = this._filterSelectedOptions(options, initialValue);
this._comboBox.filteredItems = filteredItems;
this._comboBox.setInputValue(initialValue);
} else {
this._opened = false;
}
}
private _filterChanged(ev?: CustomEvent): void {
this._filter = ev?.detail.value || "";
private _filterSelectedOptions = (
options: StateContentOption[],
current?: string
) => {
const value = this._value;
const filteredItems = this._comboBox.items?.filter((item) => {
const label = item.label || item.value;
return label.toLowerCase().includes(this._filter?.toLowerCase());
});
return options.filter(
(option) => !value.includes(option.value) || option.value === current
);
};
if (this._filter) {
filteredItems?.unshift({ label: this._filter, value: this._filter });
private _filterChanged(ev: ValueChangedEvent<string>) {
const input = ev.detail.value;
const filter = input?.toLowerCase() || "";
const options = this._comboBox.items || [];
const currentValue =
this._editIndex != null ? this._value[this._editIndex] : "";
this._comboBox.filteredItems = this._filterSelectedOptions(
options,
currentValue
);
if (!filter) {
return;
}
const fuseOptions: IFuseOptions<StateContentOption> = {
keys: ["primary", "secondary", "value"],
isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
ignoreDiacritics: true,
};
const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions);
const filteredItems = fuse.search(filter).map((result) => result.item);
this._comboBox.filteredItems = filteredItems;
}
@@ -257,43 +371,40 @@ class HaEntityStatePicker extends LitElement {
newValue.splice(newIndex, 0, element);
this._setValue(newValue);
await this.updateComplete;
this._filterChanged();
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
}
private async _removeItem(ev) {
ev.stopPropagation();
const value: string[] = [...this._value];
value.splice(ev.target.idx, 1);
const value = [...this._value];
const idx = parseInt(ev.target.dataset.idx, 10);
value.splice(idx, 1);
this._setValue(value);
await this.updateComplete;
this._filterChanged();
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
}
private _comboBoxValueChanged(ev: CustomEvent): void {
private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
ev.stopPropagation();
const newValue = ev.detail.value;
const value = ev.detail.value;
if (this.disabled || newValue === "") {
if (this.disabled || value === "") {
return;
}
const currentValue = this._value;
const newValue = [...this._value];
if (currentValue.includes(newValue)) {
return;
if (this._editIndex != null) {
newValue[this._editIndex] = value;
} else {
newValue.push(value);
}
setTimeout(() => {
this._filterChanged();
this._comboBox.setInputValue("");
}, 0);
this._setValue([...currentValue, newValue]);
this._setValue(newValue);
}
private _setValue(value: string[]) {
const newValue =
value.length === 0 ? undefined : value.length === 1 ? value[0] : value;
const newValue = this._toValue(value);
this.value = newValue;
fireEvent(this, "value-changed", {
value: newValue,
@@ -303,10 +414,64 @@ class HaEntityStatePicker extends LitElement {
static styles = css`
:host {
position: relative;
width: 100%;
}
.container {
position: relative;
background-color: var(--mdc-text-field-fill-color, whitesmoke);
border-radius: var(--ha-border-radius-sm);
border-end-end-radius: var(--ha-border-radius-square);
border-end-start-radius: var(--ha-border-radius-square);
}
.container:after {
display: block;
content: "";
position: absolute;
pointer-events: none;
bottom: 0;
left: 0;
right: 0;
height: 1px;
width: 100%;
background-color: var(
--mdc-text-field-idle-line-color,
rgba(0, 0, 0, 0.42)
);
transform:
height 180ms ease-in-out,
background-color 180ms ease-in-out;
}
.container.disabled:after {
background-color: var(
--mdc-text-field-disabled-line-color,
rgba(0, 0, 0, 0.42)
);
}
.container:focus-within:after {
height: 2px;
background-color: var(--mdc-theme-primary);
}
label {
display: block;
margin: 0 0 var(--ha-space-2);
}
.add {
order: 1;
}
mwc-menu-surface {
--mdc-menu-min-width: 100%;
}
ha-chip-set {
padding: 8px 0;
padding: var(--ha-space-2) var(--ha-space-2);
}
.invalid {
text-decoration: line-through;
}
.sortable-fallback {
@@ -326,6 +491,6 @@ class HaEntityStatePicker extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-entity-state-content-picker": HaEntityStatePicker;
"ha-entity-state-content-picker": HaStateContentPicker;
}
}

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