Compare commits

...

152 Commits

Author SHA1 Message Date
Petar Petrov
c492e84e58 Apply suggestion from @MindFreeze 2026-01-07 08:53:36 +02:00
Paul Bottein
e39ab6dfe4 Add icons 2026-01-06 18:27:59 +01:00
Paul Bottein
76f43676d5 Use tabs for bluetooth panel 2026-01-06 18:27:36 +01:00
Bram Kragten
b84a51235d Prevent showing error during loading of statistics picker (#28823) 2026-01-06 17:40:53 +01:00
Bram Kragten
602d6a2337 Use target selector to filter references entities (#28822)
* Use target selector to filter references entities

* Update ha-selector-state.ts
2026-01-06 16:16:23 +01:00
Matthias Alphart
6e614cd3f2 Explicitly set ha-wa-dialog content color (#28821) 2026-01-06 16:00:15 +01:00
Paulus Schoutsen
6793edd68b Bluetooth panel to support multi adapter (#28763)
* Support multiple adapters in bluetooth panel

* Move connection allocations up

* Make it tabs

* Add icons

* Revert "Add icons"

This reverts commit e338b6e578.

* Revert "Make it tabs"

This reverts commit d1b19d5c3e.

* Fix scanner matching and no active connection slot support

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

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-06 16:46:43 +02:00
Bram Kragten
ad6e3267c3 Fix translation loading of choose selector (#28817) 2026-01-06 14:24:19 +01:00
Bram Kragten
f941117ca4 Remove iOS focus handling from dialogs (#28818) 2026-01-06 13:45:21 +01:00
Bram Kragten
aef0bf03e3 Use single path for thread icon, add KNX, simplify (#28819)
* Use single path for thread icon, simplify

* Add custom path for KNX
2026-01-06 12:43:05 +00:00
Simon Lamon
f22f6b74db Remove used from energy usage header (#28775)
* Remove used

* Update src/translations/en.json

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-06 11:37:27 +00:00
Aidan Timson
913c4ae24e Remove duplicate custom items, remove "no matching ..." when allow-custom-value set (#28801)
* Remove duplicate custom items, allow default from picker

* Memoize

* Memoize

* Memoize func

* Don't show no matching item when custom value is allowed

* Remove no items found label now unused

* Cleanup unused translations

* Restore used value

* Remove no items found label now unused

* Remove redundant comment

* Remove searchFn

* Ensure custom value isnt identical

* Fix duplicated value

* Fix duplicated value

* Use additional items for entity state content

* Fix duplicate values

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2026-01-06 11:16:54 +01:00
Matthias Alphart
4b7b5fa21a Replace unload event handler for custom panels with pagehide (#28781)
* Replace `unload` event handler for custom panels

* Handle restore from bfcache

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-06 09:07:52 +00:00
Norbert Rittel
bf6887541b Capitalize counter button labels (#28814)
Capitalized counter button labels
2026-01-06 08:47:51 +02:00
karwosts
26da9f3a37 Fix statistic-graph-card cutoff w/ energy date picker (#28810)
* Fix statistics-graph energy-date mode end-time with 5min statistics

* don't modify date/hour for 5minute graph

* suggestedMax use period instead of days

* go back to string types
2026-01-05 17:28:15 +02:00
Aidan Timson
d48520efdf Add option for any state and show translated label for entity state values (#28803)
* Add option for any state

* Use translated labels for value
2026-01-05 16:55:23 +02:00
Aidan Timson
d462356122 Reapply "Migrate dialog-device-registry-detail to ha-wa-dialog (#27668)" (#28804)
* Reapply "Migrate dialog-device-registry-detail to ha-wa-dialog (#27668)" (#27716)

This reverts commit 5f75fc5bcb.

* Apply suggestion from @MindFreeze

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-05 11:51:12 +00:00
Copilot
9a5cdb0a99 Display template targets with neutral badge instead of "Unknown area" error (#28799)
* Initial plan

* Add template target display with neutral badge

- Import mdiCodeBraces icon and isTemplate function
- Check if target ID is a template before checking if it exists
- Display grey {} icon with "Template" text for templated targets
- Add "template" translation key to target_summary
- Prevents misleading red "Unknown area" badge for template targets

Co-authored-by: piitaya <5878303+piitaya@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: piitaya <5878303+piitaya@users.noreply.github.com>
2026-01-05 12:44:58 +01:00
Paul Bottein
eaf012d5ff Show close button when zwave firmware update is finished (#28805) 2026-01-05 13:42:55 +02:00
Paul Bottein
19934dad72 Remove custom value for unknown icon in icon picker (#28800) 2026-01-05 10:57:09 +00:00
Paul Bottein
6194f73442 Use regular item for bottom padding in combobox (#28798) 2026-01-05 11:40:54 +01:00
Paul Bottein
dbc880fe35 Add warning about running tsc with file arguments (#28797)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 10:08:21 +00:00
karwosts
be4e46a3c6 Fix statistic names w/ energy_date_selection (#28787) 2026-01-05 09:51:49 +02:00
renovate[bot]
2fce89a689 Update dependency globals to v17 (#28789)
* Update dependency globals to v17

* Add global definitions for audioWorklet in ESLint configuration

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-05 07:11:10 +00:00
renovate[bot]
81d21b0907 Update formatjs monorepo (#28793)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-05 09:04:20 +02:00
dependabot[bot]
65381b1dc5 Bump relative-ci/agent-action from 3.2.1 to 3.2.2 (#28792)
Bumps [relative-ci/agent-action](https://github.com/relative-ci/agent-action) from 3.2.1 to 3.2.2.
- [Release notes](https://github.com/relative-ci/agent-action/releases)
- [Commits](c45aaa919e...3c68192601)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-05 08:51:48 +02:00
Norbert Rittel
7cbede2f6e A few small spelling fixes in user-facing strings (#28786)
- use correct spelling for "Wi-Fi" trademark
- capitalize "PIN" as abbreviation
- fix spelling of "set up" as verb
- fix sentence-casing
2026-01-04 18:07:40 +01:00
renovate[bot]
0a13dddaea Update dependency @rspack/core to v1.7.0 (#28774)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-04 09:12:12 +00:00
renovate[bot]
662be980e8 Update dependency @rspack/dev-server to v1.1.5 (#28773)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-04 10:03:10 +01:00
renovate[bot]
209abf466d Update dependency @codemirror/view to v6.39.8 (#28759)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-02 19:29:57 +01:00
Simon Lamon
db9a3bd562 Fix matter translations (#28752) 2026-01-02 11:22:45 +01:00
Paulus Schoutsen
36ecaa6610 Add config entry picker for Z-Wave JS panel (#28741) 2026-01-02 11:20:42 +01:00
Simon Lamon
4f46d0f4a3 Make cancel a secondary action in blueprint import (#28754) 2026-01-02 11:18:37 +01:00
Paulus Schoutsen
42ad47649d Verify bluetooth config entries exist before showing entry (#28745) 2026-01-02 11:18:02 +01:00
dependabot[bot]
c62ee6e692 Bump qs from 6.14.0 to 6.14.1 (#28760)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 11:16:37 +01:00
Simon Lamon
b38c8d7d5f Revert lit update (#28751) 2026-01-02 11:13:09 +01:00
renovate[bot]
83bcc39d5f Update dependency typescript-eslint to v8.51.0 (#28756) 2026-01-02 09:54:41 +01:00
Paulus Schoutsen
8d317d1e2c Hide dashboard controls in kiosk mode (#28742) 2026-01-01 00:36:49 +01:00
Simon Lamon
9acad2e83c Provide kioskmode in demo (#28739) 2025-12-30 22:45:01 +01:00
ildar170975
9099c5a92c Map card editor: add a basic sub-element editor (#28687)
* add subelement editor

* explicit type convertion

* test

* test

* test

* test

* prettier
2025-12-30 20:18:57 +01:00
Paulus Schoutsen
60c4d60d66 Protocol link updates (#28736)
* Update icons Thread & Insteon

* Remove matter link

* Remove back path from ZHA

* Fix ZHA dashboard config entry
2025-12-30 19:54:48 +01:00
sebcaps
e8a4cde643 Add energy percentage usage on pie chart view. (#28733)
* showPercent

* unnecessary change
2025-12-30 19:54:35 +01:00
renovate[bot]
148eab31b6 Update dependency jsdom to v27.4.0 (#28726)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 19:18:29 +01:00
Franck Nijhof
9f7683021e Bumped version to 20251229.0 2025-12-29 12:41:19 +00:00
Franck Nijhof
549bd25f5c Merge branch 'master' into dev 2025-12-29 12:38:44 +00:00
Paulus Schoutsen
eb74dd541a Show the protocols on the top level of the config section (#28448) 2025-12-29 11:52:20 +01:00
Paulus Schoutsen
4c84c7b54f Add kiosk mode foundation (#28714)
* Add kiosk mode foundation

* last file too
2025-12-29 11:24:24 +01:00
renovate[bot]
3a8f964ebd Update formatjs monorepo (#28724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 05:21:27 +00:00
Christopher Nethercott
211ddcf7a4 Developer Tools: Update both event fire and event listen when clicked. (#28646)
Set the event listen event to one clicked when it isn't currently listening to an event.
2025-12-28 20:40:45 +01:00
renovate[bot]
021e5f5ce0 Update dependency @swc/helpers to v0.5.18 (#28722)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-28 19:03:16 +01:00
Bram Kragten
5b0bd9d577 Merge branch 'rc' 2025-12-19 17:05:32 +01:00
Bram Kragten
d839152fd1 Bumped version to 20251203.3 2025-12-19 17:05:16 +01:00
Petar Petrov
407cb79805 Fix power sources graph ordering with multiple sources (#28549) 2025-12-19 17:04:50 +01:00
karwosts
7817ebe983 Home strategy: don't link non-admin to config pages (#28512) 2025-12-19 17:04:49 +01:00
Wendelin
7e58cedd49 Fix ha-toast z-index (#28491) 2025-12-19 17:04:48 +01:00
Wendelin
06334a039c Fix automation add TCA search icons (#28490)
Fix automation add TCA seach icons
2025-12-19 17:04:47 +01:00
Silas Krause
6e5853a1c0 Support legacy table styles in markdown (#28488)
* Remove unnecessary assist styles

* Fix list styles

* Remove table styles for role="presentation"
2025-12-19 17:04:46 +01:00
Wendelin
f4f4520773 Fix target picker area in history/activity (#28474)
* Add max target picker width for history and activity

* Fix target picker  area selection in history and activity
2025-12-19 17:04:45 +01:00
karwosts
94453dfba5 Fix markdown card image sizing (#28449) 2025-12-19 17:04:44 +01:00
Paul Bottein
0ce0247a2c 20251203.2 (#28443) 2025-12-08 17:30:04 +01:00
Paul Bottein
ce8cabbad9 Bumped version to 20251203.2 2025-12-08 17:29:01 +01:00
karwosts
0802841606 More unsafe description_placeholders fixes (#28416) 2025-12-08 17:28:52 +01:00
Nils Schönwald
cb93e1b741 Update snowflake to 6 sides (#28406) 2025-12-08 17:28:51 +01:00
dcapslock
30c383a2fc Energy strategies to refresh energy collection which allows to be used in custom dashboards (#28400)
* Energy strategies to refresh energy collection which allows to be used in custom dashboards

* Update src/panels/energy/strategies/energy-overview-view-strategy.ts

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

* Only refresh if no prefs

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-08 17:28:50 +01:00
karwosts
73ee235fef Fix for undefined description_placeholders (#28395)
Another fix for undefined description_placeholders
2025-12-08 17:28:49 +01:00
Paul Bottein
31603ea7b2 20251203.1 (#28383) 2025-12-05 20:53:17 +01:00
Paul Bottein
17c1043cfc Bumped version to 20251203.1 2025-12-05 20:51:48 +01:00
Timothy
da255dce40 Add add to button in more info topbar for non admin users (#28365) 2025-12-05 20:51:20 +01:00
Paul Bottein
0c68072f8f Use non-admin endpoint to subscribe to one lab feature (#28352) 2025-12-05 20:51:19 +01:00
Petar Petrov
d197fd8f76 Fix calendar card not showing different colors for multiple calendars (#28338) 2025-12-05 20:51:18 +01:00
Paul Bottein
a961a87872 Move reorder areas and floors to floor overflow (#28335) 2025-12-05 20:51:17 +01:00
Petar Petrov
cc96c707b9 Fix markdown sections and styling (#28333) 2025-12-05 20:51:16 +01:00
Petar Petrov
4b73713f2a Fix gauge severity using entity state instead of attribute value (#28331) 2025-12-05 20:51:15 +01:00
Petar Petrov
c001102f15 Append current state to power-sources-graph (#28330) 2025-12-05 20:51:14 +01:00
Preet Patel
c1e5e0bfcb Fix energy dashboard redirect for device-consumption-only configs (#28322)
When users configure energy with only device consumption (no
grid/solar/battery/gas/water sources), the dashboard would redirect
to /config/energy instead of displaying. This occurred because
_generateLovelaceConfig() returned an empty views array.

The fix adds hasDeviceConsumption check and includes ENERGY_VIEW
when device consumption is configured, since energy-view-strategy
already supports device consumption cards.
2025-12-05 20:51:13 +01:00
Bram Kragten
a1412e90fd Add more info to the energy demo (#28316)
* Add more info to the energy demo

* Also add battery power
2025-12-05 20:51:12 +01:00
Petar Petrov
f6f40c1679 Always show energy-sources-table in overview (#28315) 2025-12-05 20:48:59 +01:00
Bram Kragten
d77bebe96b Bumped version to 20251203.0 2025-12-03 15:38:49 +01:00
Bram Kragten
1260af0b45 Fix add matter device my link (#28313) 2025-12-03 15:36:05 +01:00
Petar Petrov
1d37eec411 Fix label filter losing selections when searching (#28312) 2025-12-03 15:36:04 +01:00
Bram Kragten
5a52f83358 Fix sticky headers in TCA dialog when target is selected (#28310) 2025-12-03 15:36:03 +01:00
Aidan Timson
60724eb952 Add subscribeLabFeature function (#28309)
* Add subscribe to lab feature function

* Add docstrings to exported functions
2025-12-03 15:36:02 +01:00
Aidan Timson
de5778079e Add small rotation to snowflakes (#28308) 2025-12-03 15:36:01 +01:00
Wendelin
f3710650f2 Hide disabled devices in automation target tree (#28307) 2025-12-03 15:36:00 +01:00
Paul Bottein
feb35dbc4f Use svg for snowflakes (#28306) 2025-12-03 15:35:59 +01:00
Paul Bottein
ee9e101fa6 Rename unassigned areas to other areas (#28305) 2025-12-03 15:35:58 +01:00
Paul Bottein
24b16360a6 Use core area sorting everywhere (#28304) 2025-12-03 15:35:57 +01:00
Wendelin
109c81a00d Revert "Migrate updates dropdown to ha-dropdown" (#28303)
Revert "Migrate updates dropdown to ha-dropdown (#28039)"

This reverts commit ba9bab38c9.
2025-12-03 15:35:56 +01:00
Wendelin
eaa1ddbf61 Fix filtering of floors in getAreasAndFloorsItems function (#28302) 2025-12-03 15:35:55 +01:00
Paul Bottein
b11cb57a1e Always set ha-wa-dialog position to fixed (#28301) 2025-12-03 15:35:55 +01:00
Petar Petrov
87b5f58779 Add Y-axis label formatter to energy charts (#28298) 2025-12-03 15:35:53 +01:00
Petar Petrov
8dac53c672 Fix binary sensor history timeline not rendering properly (#28297) 2025-12-03 15:35:52 +01:00
Petar Petrov
d0966bf35a Hide empty System message in assist debug view (#28296) 2025-12-03 15:35:51 +01:00
Paul Bottein
6ba4fc0808 Handle not existing panels in dashboard config (#28292) 2025-12-03 15:35:50 +01:00
ildar170975
bd582ff816 computeLovelaceEntityName(): allow "number" names to be processed (#28231)
* allow "number" names to be processed

* Apply suggestion from @MindFreeze

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-03 15:35:49 +01:00
Bram Kragten
d34bf83da0 Bumped version to 20251202.0 2025-12-02 16:02:32 +01:00
Wendelin
b0cfb31bf3 Automation add TCA: fix narrow subtitles & icons (#28291) 2025-12-02 16:02:25 +01:00
Wendelin
6c39e5d2c5 Use history to manage back button click in automations add TCA (#28289) 2025-12-02 16:02:24 +01:00
Paul Bottein
7b51e71092 Only show current weather in home overview (#28288) 2025-12-02 16:02:23 +01:00
Paul Bottein
8a82483685 Fix container alignment in section view (#28287) 2025-12-02 16:02:23 +01:00
Bram Kragten
bb691fa7a2 fix paste in add tca dialog (#28286) 2025-12-02 16:02:22 +01:00
Petar Petrov
2232db9c0f Update Energy dashboard layout (#28283) 2025-12-02 16:02:21 +01:00
Petar Petrov
5375665dc6 Fix index value for grid return in power sankey card (#28281) 2025-12-02 16:02:20 +01:00
Silas Krause
480122f40a Revert custom markdown styles (#28277) 2025-12-02 16:02:18 +01:00
karwosts
ee5c54030a Safer lookup of description_placeholders when service is invalid (#28273) 2025-12-02 16:02:17 +01:00
Paul Bottein
b73f50e864 Add dialog to reorder areas and floors (#28272) 2025-12-02 16:02:16 +01:00
eringerli
b9836073b7 fix stacking of multiple power sources (#28243) 2025-12-02 16:02:15 +01:00
Bram Kragten
a40512e0b5 Bumped version to 20251201.0 2025-12-01 16:35:54 +01:00
Paul Bottein
b2122570fb Clean reference to floor compare (#28269)
Fix floor compare
2025-12-01 16:35:34 +01:00
Paul Bottein
885f9333d2 Add helper for floor level (#28268)
* Add helper for floor level

* Update src/translations/en.json

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

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-01 16:35:33 +01:00
Aidan Timson
f812e7e9fb Match more-info-update backup preferences (#28266) 2025-12-01 16:35:32 +01:00
Wendelin
64dad39f6e Fix automation trigger ha icon (#28265) 2025-12-01 16:35:31 +01:00
Simon Lamon
df0fb423ed Include background in light, climate and security views (#28264)
* Include background

* Remove background key

* Add imports
2025-12-01 16:35:30 +01:00
Wendelin
4c3156f290 Respect system area sort in automation target tree (#28263) 2025-12-01 16:35:29 +01:00
Petar Petrov
ecdf374902 Reduce the duration of init animation for charts to 500ms (#28262)
Reduce the duration of init animation for charts
2025-12-01 16:35:29 +01:00
Aidan Timson
3e924e0cde Add missing key for labs to show in quick bar (#28261) 2025-12-01 16:35:27 +01:00
Bram Kragten
6fb71e12c8 Use name instead of description_configured for triggers and conditions (#28260) 2025-12-01 16:35:27 +01:00
Wendelin
6138aa5489 Fix ha-bottom-sheet closed event (#28257) 2025-12-01 16:35:26 +01:00
Aidan Timson
61e865d3a6 Fix 1px padding for subpage titles (#28256) 2025-12-01 16:35:24 +01:00
Aidan Timson
febcbf6242 Make labs toolbar icon use default color (#28255) 2025-12-01 16:35:23 +01:00
Petar Petrov
6a2fac6a9e Fix refresh in energy panel subviews (#28252) 2025-12-01 16:35:22 +01:00
karwosts
b60c5467fc Add water devices to energy data download (#28242) 2025-12-01 16:35:21 +01:00
Petar Petrov
ecd563406e Add power view and restructure energy dashboard layout (#28240) 2025-12-01 16:35:19 +01:00
Silas Krause
d5b66315e2 Fix markdown rendering for cached html (#28229)
* Render markdown table in wrapper.

* Fix markdown styles

* Fix formatting

* fix rendering for cache
2025-12-01 16:35:18 +01:00
karwosts
5b1719fc6e Add missing helper to language selector (#28218) 2025-12-01 16:35:17 +01:00
Silas Krause
add22cf2e9 Fix markdown styles regression (#28202)
* Render markdown table in wrapper.

* Fix markdown styles

* Fix formatting
2025-12-01 16:35:16 +01:00
Paul Bottein
21509191fa Fix ha icon size (#28201) 2025-12-01 16:35:15 +01:00
Paul Bottein
1a73cccf0d Fix safe area for sidebar section views in Android (#28194) 2025-12-01 16:35:14 +01:00
Aidan Timson
407d68250a Fix ha-wa-dialog fullscreen and make alerts not fullscreen (#28175) 2025-12-01 16:35:13 +01:00
Bram Kragten
38b7bd18bb Bumped version to 20251127.0 2025-11-27 17:06:57 +01:00
Wendelin
a00e944a35 Add TCA by target sort like item collections (#28192) 2025-11-27 17:06:30 +01:00
Petar Petrov
481569804e Fix water sankey calculation to include total supply from sources (#28191) 2025-11-27 17:06:29 +01:00
Paul Bottein
a1d7e270ff Add hint to reorder areas and floors (#28189) 2025-11-27 17:06:28 +01:00
Wendelin
225ccf1d2f Fix lab automations icons and sidebar width (#28184)
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-27 17:06:27 +01:00
Wendelin
4a5e1f9f3f "Add TCA" dialog desktop height to 800px (#28182) 2025-11-27 17:06:26 +01:00
Wendelin
b27b7210fd Show hidden entities in target tree (#28181)
* Show hidden entities in target tree

* Fix types
2025-11-27 17:06:25 +01:00
Petar Petrov
acd5181449 Fix sankey chart resizing (#28180) 2025-11-27 17:06:24 +01:00
Bram Kragten
b6b2d03a80 Always store token when using develop and serve (#28179) 2025-11-27 17:06:22 +01:00
Paul Bottein
7aee2b7cb7 Fix labs back button (#28174) 2025-11-27 17:06:21 +01:00
Paul Bottein
df1914cb7a Fix disabled dashboard picker when no custom dashboard (#28172) 2025-11-27 17:06:20 +01:00
Paul Bottein
6706d5904d Fix box shadow for sidebar tabs (#28170) 2025-11-27 17:06:19 +01:00
Wendelin
221aefd764 Fix automation add TCA autofocus (#28168)
Fix automation add tca autofocus
2025-11-27 17:06:18 +01:00
Paul Bottein
670057e8e6 Restore sidebar view when clicking back (#28167) 2025-11-27 17:06:17 +01:00
Wendelin
427e46201c Fix add condition default tab and blank styles (#28166) 2025-11-27 17:06:16 +01:00
Petar Petrov
fd1240f335 Refactor power sankey hierarchy to handle devices with not power sensor (#28164) 2025-11-27 17:06:15 +01:00
Petar Petrov
aa7670cb59 Disable axis pointer on the energy devices bar chart to fix refresh issues on touch devices (#28163) 2025-11-27 17:06:14 +01:00
Petar Petrov
468139229c Handle grouping by floor and area in power sankey card (#28162) 2025-11-27 17:06:13 +01:00
Simon Lamon
39752f0e3f Don't show more info for untracked consumption (#28151) 2025-11-27 17:06:12 +01:00
Petar Petrov
4d850d067f Replace gauges with energy usage graph in energy overview (#28150) 2025-11-27 17:06:10 +01:00
Paul Bottein
bcae64df88 Use hui-root for panel energy (#28149)
* Use hui-root for panel energy

* Review feedback

* Set empty prefs
2025-11-27 17:06:09 +01:00
Iván Pereira
690fd5a061 Fix hide sidebar tooltip on touchend events (#28042)
* fix: hide sidebar tooltip on touchend events

* Add a comment recommended by Copilot

* Clear timeouts id in disconnectedCallback
2025-11-27 17:06:08 +01:00
Bram Kragten
ac56c6df9a Bumped version to 20251126.0 2025-11-26 16:11:20 +01:00
57 changed files with 1475 additions and 1113 deletions

View File

@@ -22,11 +22,13 @@ You are an assistant helping with development of the Home Assistant frontend. Th
```bash
yarn lint # ESLint + Prettier + TypeScript + Lit
yarn format # Auto-fix ESLint + Prettier
yarn lint:types # TypeScript compiler
yarn lint:types # TypeScript compiler (run WITHOUT file arguments)
yarn test # Vitest
script/develop # Development server
```
> **WARNING:** Never run `tsc` or `yarn lint:types` with file arguments (e.g., `yarn lint:types src/file.ts`). When `tsc` receives file arguments, it ignores `tsconfig.json` and emits `.js` files into `src/`, polluting the codebase. Always run `yarn lint:types` without arguments. For individual file type checking, rely on IDE diagnostics. If `.js` files are accidentally generated, clean up with `git clean -fd src/`.
### Component Prefixes
- `ha-` - Home Assistant components

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@c45aaa919ef85620af54242a241ac17a8fa35983 # v3.2.1
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}

View File

@@ -187,5 +187,11 @@ export default tseslint.config(
],
"no-use-before-define": "off",
},
},
{
files: ["src/util/recorder-worklet.js"],
languageOptions: {
globals: globals.audioWorklet,
},
}
);

View File

@@ -34,18 +34,18 @@
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.3",
"@codemirror/view": "6.39.7",
"@codemirror/view": "6.39.8",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.0.6",
"@formatjs/intl-displaynames": "7.0.6",
"@formatjs/intl-durationformat": "0.8.6",
"@formatjs/intl-getcanonicallocales": "3.0.4",
"@formatjs/intl-listformat": "8.0.6",
"@formatjs/intl-locale": "5.0.6",
"@formatjs/intl-numberformat": "9.0.7",
"@formatjs/intl-pluralrules": "6.0.6",
"@formatjs/intl-relativetimeformat": "12.0.7",
"@formatjs/intl-datetimeformat": "7.1.1",
"@formatjs/intl-displaynames": "7.1.1",
"@formatjs/intl-durationformat": "0.9.1",
"@formatjs/intl-getcanonicallocales": "3.1.1",
"@formatjs/intl-listformat": "8.1.1",
"@formatjs/intl-locale": "5.1.1",
"@formatjs/intl-numberformat": "9.1.1",
"@formatjs/intl-pluralrules": "6.1.1",
"@formatjs/intl-relativetimeformat": "12.1.1",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
"@fullcalendar/interaction": "6.1.20",
@@ -85,7 +85,7 @@
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.17",
"@swc/helpers": "0.5.18",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0",
@@ -112,13 +112,13 @@
"hls.js": "1.6.15",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "11.0.6",
"intl-messageformat": "11.0.8",
"js-yaml": "4.1.1",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"leaflet.markercluster": "1.5.3",
"lit": "3.3.2",
"lit-html": "3.3.2",
"lit": "3.3.1",
"lit-html": "3.3.1",
"luxon": "3.7.2",
"marked": "17.0.1",
"memoize-one": "6.0.0",
@@ -156,8 +156,8 @@
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.4.0",
"@rspack/core": "1.6.8",
"@rspack/dev-server": "1.1.4",
"@rspack/core": "1.7.0",
"@rspack/dev-server": "1.1.5",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.25",
"@types/chromecast-caf-sender": "1.0.11",
@@ -199,7 +199,7 @@
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "27.3.0",
"jsdom": "27.4.0",
"jszip": "3.10.1",
"lint-staged": "16.2.7",
"lit-analyzer": "2.0.3",
@@ -215,7 +215,7 @@
"terser-webpack-plugin": "5.3.16",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.50.1",
"typescript-eslint": "8.51.0",
"vite-tsconfig-paths": "6.0.3",
"vitest": "4.0.16",
"webpack-stats-plugin": "1.1.3",
@@ -224,12 +224,12 @@
},
"resolutions": {
"@material/mwc-button@^0.25.3": "^0.27.0",
"lit": "3.3.2",
"lit-html": "3.3.2",
"lit": "3.3.1",
"lit-html": "3.3.1",
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.2",
"@fullcalendar/daygrid": "6.1.20",
"globals": "16.5.0",
"globals": "17.0.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",
"glob@^10.2.2": "^10.5.0"

View File

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

View File

@@ -18,10 +18,7 @@ import "../ha-combo-box-item";
import "../ha-generic-picker";
import type { HaGenericPicker } from "../ha-generic-picker";
import "../ha-input-helper-text";
import {
NO_ITEMS_AVAILABLE_ID,
type PickerComboBoxItem,
} from "../ha-picker-combo-box";
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
import "../ha-sortable";
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
@@ -184,18 +181,17 @@ export class HaEntityNamePicker extends LitElement {
.disabled=${this.disabled}
.required=${this.required && !value.length}
.getItems=${this._getFilteredItems}
.getAdditionalItems=${this._getAdditionalItems}
.rowRenderer=${rowRenderer}
.searchFn=${this._searchFn}
.notFoundLabel=${this.hass.localize(
"ui.components.entity.entity-name-picker.no_match"
)}
.value=${this._getPickerValue()}
allow-custom-value
.customValueLabel=${this.hass.localize(
"ui.components.entity.entity-name-picker.custom_name"
)}
@value-changed=${this._pickerValueChanged}
.searchFn=${this._searchFn}
.searchLabel=${this.hass.localize(
"ui.components.entity.entity-name-picker.search"
)}
>
<div slot="field" class="container">
<ha-sortable
@@ -316,10 +312,7 @@ export class HaEntityNamePicker extends LitElement {
return undefined;
}
private _getFilteredItems = (
searchString?: string,
_section?: string
): PickerComboBoxItem[] => {
private _getFilteredItems = (): PickerComboBoxItem[] => {
const items = this._getItems(this.entityId);
const currentItem =
this._editIndex != null ? this._items[this._editIndex] : undefined;
@@ -336,49 +329,27 @@ export class HaEntityNamePicker extends LitElement {
);
// When editing an existing text item, include it in the base items
if (currentItem?.type === "text" && currentItem.text && !searchString) {
if (currentItem?.type === "text" && currentItem.text) {
filteredItems.push(this._customNameOption(currentItem.text));
}
return filteredItems;
};
private _getAdditionalItems = (
searchString?: string
private _searchFn = (
searchString: string,
filteredItems: PickerComboBoxItem[]
): PickerComboBoxItem[] => {
if (!searchString) {
return [];
}
const currentItem =
this._editIndex != null ? this._items[this._editIndex] : undefined;
const currentId =
currentItem?.type === "text" && currentItem.text
? this._customNameOption(currentItem.text).id
: undefined;
// Don't add if it's the same as the current item being edited
if (
currentItem?.type === "text" &&
currentItem.text &&
currentItem.text === searchString
) {
return [];
}
// Always return custom name option when there's a search string
// This prevents "No matching items found" from showing
return [this._customNameOption(searchString)];
};
private _searchFn = (
search: string,
filteredItems: PickerComboBoxItem[],
_allItems: PickerComboBoxItem[]
): PickerComboBoxItem[] => {
// Remove NO_ITEMS_AVAILABLE_ID if we have additional items (custom name option)
// This prevents "No matching items found" from showing when custom values are allowed
const hasAdditionalItems = this._getAdditionalItems(search).length > 0;
if (hasAdditionalItems) {
return filteredItems.filter(
(item) => typeof item !== "string" || item !== NO_ITEMS_AVAILABLE_ID
);
// Remove custom name option if search string is present to avoid duplicates
if (searchString && currentId) {
return filteredItems.filter((item) => item.id !== currentId);
}
return filteredItems;
};

View File

@@ -19,7 +19,10 @@ import "../ha-combo-box-item";
import "../ha-generic-picker";
import type { HaGenericPicker } from "../ha-generic-picker";
import "../ha-input-helper-text";
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
import {
NO_ITEMS_AVAILABLE_ID,
type PickerComboBoxItem,
} from "../ha-picker-combo-box";
import "../ha-sortable";
const HIDDEN_ATTRIBUTES = [
@@ -199,11 +202,7 @@ export class HaStateContentPicker extends LitElement {
.value=${this._getPickerValue()}
.getItems=${this._getFilteredItems}
.getAdditionalItems=${this._getAdditionalItems}
.notFoundLabel=${this.hass.localize("ui.components.combo-box.no_match")}
allow-custom-value
.customValueLabel=${this.hass.localize(
"ui.components.entity.entity-state-content-picker.custom_state"
)}
.searchFn=${this._searchFn}
@value-changed=${this._pickerValueChanged}
>
<div slot="field" class="container">
@@ -328,7 +327,7 @@ export class HaStateContentPicker extends LitElement {
(text: string): PickerComboBoxItem => ({
id: text,
primary: this.hass.localize(
"ui.components.entity.entity-state-content-picker.custom_state"
"ui.components.entity.entity-state-content-picker.custom_attribute"
),
secondary: `"${text}"`,
search_labels: {
@@ -340,10 +339,7 @@ export class HaStateContentPicker extends LitElement {
})
);
private _getFilteredItems = (
searchString?: string,
_section?: string
): PickerComboBoxItem[] => {
private _getFilteredItems = (): PickerComboBoxItem[] => {
const stateObj = this.entityId
? this.hass.states[this.entityId]
: undefined;
@@ -358,11 +354,7 @@ export class HaStateContentPicker extends LitElement {
);
// When editing an existing custom value, include it in the base items
if (
currentValue &&
!items.find((item) => item.id === currentValue) &&
!searchString
) {
if (currentValue && !items.find((item) => item.id === currentValue)) {
filteredItems.push(this._customValueOption(currentValue));
}
@@ -372,33 +364,34 @@ export class HaStateContentPicker extends LitElement {
private _getAdditionalItems = (
searchString?: string
): PickerComboBoxItem[] => {
if (!searchString) {
return [];
}
const currentValue =
this._editIndex != null ? this._value[this._editIndex] : undefined;
// Don't add if it's the same as the current item being edited
if (currentValue && currentValue === searchString) {
return [];
}
// Check if the search string matches an existing item
const stateObj = this.entityId
? this.hass.states[this.entityId]
: undefined;
const items = this._getItems(this.entityId, stateObj, this.allowName);
const existingItem = items.find((item) => item.id === searchString);
// Only return custom value option if it doesn't match an existing item
if (!existingItem) {
// If the search string does not match with the id of any of the items,
// offer to add it as a custom attribute
const existingItem = items.find((item) => item.id === searchString);
if (searchString && !existingItem) {
return [this._customValueOption(searchString)];
}
return [];
};
private _searchFn = (
search: string,
filteredItems: PickerComboBoxItem[],
_allItems: PickerComboBoxItem[]
): PickerComboBoxItem[] => {
if (!search) {
return filteredItems;
}
// Always exclude NO_ITEMS_AVAILABLE_ID (since custom values are allowed) and currentValue (the custom value being edited)
return filteredItems.filter((item) => item.id !== NO_ITEMS_AVAILABLE_ID);
};
private async _moveItem(ev: CustomEvent) {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;

View File

@@ -7,6 +7,7 @@ import { getStates } from "../../common/entity/get_states";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-generic-picker";
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
import type { PickerValueRenderer } from "../ha-picker-field";
@customElement("ha-entity-state-picker")
export class HaEntityStatePicker extends LitElement {
@@ -108,6 +109,12 @@ export class HaEntityStatePicker extends LitElement {
this.extraOptions
);
private _valueRenderer: PickerValueRenderer = (value: string) => {
const items = this._getFilteredItems();
const item = items.find((option) => option.id === value);
return html`<span slot="headline">${item?.primary ?? value}</span>`;
};
protected render() {
if (!this.hass) {
return nothing;
@@ -125,6 +132,7 @@ export class HaEntityStatePicker extends LitElement {
.helper=${this.helper}
.value=${this.value}
.getItems=${this._getFilteredItems}
.valueRenderer=${this._valueRenderer}
.notFoundLabel=${this.hass.localize("ui.components.combo-box.no_match")}
.customValueLabel=${this.hass.localize(
"ui.components.entity.entity-state-picker.add_custom_state"

View File

@@ -141,6 +141,7 @@ export class HaStatisticPicker extends LitElement {
private async _getStatisticIds() {
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
this._picker?.requestUpdate();
}
private _getItems = () =>
@@ -177,9 +178,9 @@ export class HaStatisticPicker extends LitElement {
entitiesOnly?: boolean,
excludeStatistics?: string[],
value?: string
): StatisticComboBoxItem[] => {
): StatisticComboBoxItem[] | undefined => {
if (!statisticIds) {
return [];
return undefined;
}
if (includeStatisticsUnitOfMeasurement) {

View File

@@ -39,7 +39,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
public getItems!: (
searchString?: string,
section?: string
) => (PickerComboBoxItem | string)[];
) => (PickerComboBoxItem | string)[] | undefined;
@property({ attribute: false, type: Array })
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];

View File

@@ -124,9 +124,6 @@ export class HaIconPicker extends LitElement {
.label=${this.label}
.value=${this._value}
.searchFn=${this._filterIcons}
.notFoundLabel=${this.hass?.localize(
"ui.components.icon-picker.no_match"
)}
popover-placement="bottom-start"
@value-changed=${this._valueChanged}
>
@@ -173,20 +170,6 @@ export class HaIconPicker extends LitElement {
}
}
// Allow preview for custom icon not in list
if (rankedItems.length === 0) {
rankedItems.push({
item: {
id: filter,
primary: filter,
icon: filter,
search_labels: { keyword: filter },
sorting_label: filter,
},
rank: 0,
});
}
return rankedItems
.sort((itemA, itemB) => itemA.rank - itemB.rank)
.map((item) => item.item);

View File

@@ -86,9 +86,11 @@ class HaMenuButton extends LitElement {
: this.narrow;
const oldShowButton =
oldNarrow || oldHass?.dockedSidebar === "always_hidden";
oldHass?.kioskMode === false &&
(oldNarrow || oldHass?.dockedSidebar === "always_hidden");
const showButton =
this.narrow || this.hass.dockedSidebar === "always_hidden";
this.hass.kioskMode === false &&
(this.narrow || this.hass.dockedSidebar === "always_hidden");
if (this.hasUpdated && oldShowButton === showButton) {
return;

View File

@@ -42,7 +42,11 @@ class HaNavigationList extends LitElement {
class=${page.iconColor ? "icon-background" : ""}
.style="background-color: ${page.iconColor || "undefined"}"
>
<ha-svg-icon .path=${page.iconPath}></ha-svg-icon>
<ha-svg-icon
.path=${page.iconPath}
.secondaryPath=${page.iconSecondaryPath}
.viewBox=${page.iconViewBox}
></ha-svg-icon>
</div>
<span slot="headline">${page.name}</span>
${this.hasSecondary

View File

@@ -55,6 +55,7 @@ export interface PickerComboBoxItem {
}
export const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
const PADDING_ID = "___padding___";
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
item
@@ -108,7 +109,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
public getItems!: (
searchString?: string,
section?: string
) => (PickerComboBoxItem | string)[];
) => PickerComboBoxItem[] | undefined;
@property({ attribute: false, type: Array })
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
@@ -150,7 +151,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@query("ha-textfield") private _searchFieldElement?: HaTextField;
@state() private _items: (PickerComboBoxItem | string)[] = [];
@state() private _items: PickerComboBoxItem[] = [];
protected get scrollableElement(): HTMLElement | null {
return this._virtualizerElement as HTMLElement | null;
@@ -160,7 +161,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@state() private _valuePinned = true;
private _allItems: (PickerComboBoxItem | string)[] = [];
private _allItems: PickerComboBoxItem[] = [];
private _selectedItemIndex = -1;
@@ -278,8 +279,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
this._sectionTitle = this.sectionTitleFunction({
firstIndex: ev.first,
lastIndex: ev.last,
firstItem: firstItem as PickerComboBoxItem | string,
secondItem: secondItem as PickerComboBoxItem | string,
firstItem: firstItem as PickerComboBoxItem,
secondItem: secondItem as PickerComboBoxItem,
itemsCount: this._virtualizerElement.items.length,
});
}
@@ -294,7 +295,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
this.getAdditionalItems?.(searchString) || [];
private _getItems = () => {
let items = [...this.getItems(this._search, this.selectedSection)];
let items = [...(this.getItems(this._search, this.selectedSection) || [])];
if (!this.sections?.length) {
items = items.sort((entityA, entityB) => {
@@ -323,28 +324,28 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
});
}
if (!items.length) {
items.push(NO_ITEMS_AVAILABLE_ID);
if (!items.length && !this.allowCustomValue) {
items.push({ id: NO_ITEMS_AVAILABLE_ID, primary: "" });
}
const additionalItems = this._getAdditionalItems();
items.push(...additionalItems);
if (this.mode === "dialog") {
items.push("padding"); // padding for safe area inset
items.push({ id: PADDING_ID, primary: "" }); // padding for safe area inset
}
return items;
};
private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
private _renderItem = (item: PickerComboBoxItem, index: number) => {
if (!item) {
return nothing;
}
if (item === "padding") {
if (item.id === PADDING_ID) {
return html`<div class="bottom-padding"></div>`;
}
if (item === NO_ITEMS_AVAILABLE_ID) {
if (item.id === NO_ITEMS_AVAILABLE_ID) {
return html`
<div class="combo-box-row">
<ha-combo-box-item type="text" compact>
@@ -419,21 +420,18 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
return;
}
const index = this._fuseIndex(
this._allItems as PickerComboBoxItem[],
this.searchKeys
);
const index = this._fuseIndex(this._allItems, this.searchKeys);
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
this._allItems as PickerComboBoxItem[],
this._allItems,
searchString,
this.searchKeys || DEFAULT_SEARCH_KEYS,
(item) => item.id,
index
) as (PickerComboBoxItem | string)[];
);
if (!filteredItems.length) {
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
if (!filteredItems.length && !this.allowCustomValue) {
filteredItems.push({ id: NO_ITEMS_AVAILABLE_ID, primary: "" });
}
const additionalItems = this._getAdditionalItems(searchString);
@@ -442,8 +440,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
if (this.searchFn) {
filteredItems = this.searchFn(
searchString,
filteredItems as PickerComboBoxItem[],
this._allItems as PickerComboBoxItem[]
filteredItems,
this._allItems
);
}
@@ -459,7 +457,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
});
}
this._items = filteredItems as PickerComboBoxItem[];
this._items = filteredItems;
}
this._selectedItemIndex = -1;

View File

@@ -54,7 +54,8 @@ export class HaChooseSelector extends LitElement {
size="small"
.buttons=${this._toggleButtons(
this.selector.choose.choices,
this.selector.choose.translation_key
this.selector.choose.translation_key,
this.hass.localize
)}
.active=${this._activeChoice}
@value-changed=${this._choiceChanged}
@@ -72,7 +73,11 @@ export class HaChooseSelector extends LitElement {
}
private _toggleButtons = memoizeOne(
(choices: ChooseSelector["choose"]["choices"], translationKey?: string) =>
(
choices: ChooseSelector["choose"]["choices"],
translationKey?: string,
_localize?: HomeAssistant["localize"]
) =>
Object.keys(choices).map((choice) => ({
label:
this.localizeValue && translationKey

View File

@@ -1,12 +1,17 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { StateSelector } from "../../data/selector";
import { extractFromTarget } from "../../data/target";
import memoizeOne from "memoize-one";
import {
resolveEntityIDs,
type StateSelector,
type TargetSelector,
} from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types";
import "../entity/ha-entity-state-picker";
import "../entity/ha-entity-states-picker";
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
@customElement("ha-selector-state")
export class HaSelectorState extends SubscribeMixin(LitElement) {
@@ -28,16 +33,33 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
filter_attribute?: string;
filter_entity?: string | string[];
filter_target?: HassServiceTarget;
target_selector?: TargetSelector;
};
@state() private _entityIds?: string | string[];
private _convertExtraOptions = memoizeOne(
(
extraOptions?: { label: string; value: any }[]
): PickerComboBoxItem[] | undefined => {
if (!extraOptions) {
return undefined;
}
return extraOptions.map((option) => ({
id: option.value,
primary: option.label,
sorting_label: option.label,
}));
}
);
willUpdate(changedProps) {
if (changedProps.has("selector") || changedProps.has("context")) {
this._resolveEntityIds(
this.selector.state?.entity_id,
this.context?.filter_entity,
this.context?.filter_target
this.context?.filter_target,
this.context?.target_selector
).then((entityIds) => {
this._entityIds = entityIds;
});
@@ -45,6 +67,9 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
}
protected render() {
const extraOptions = this._convertExtraOptions(
this.selector.state?.extra_options
);
if (this.selector.state?.multiple) {
return html`
<ha-entity-states-picker
@@ -52,7 +77,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
.entityId=${this._entityIds}
.attribute=${this.selector.state?.attribute ||
this.context?.filter_attribute}
.extraOptions=${this.selector.state?.extra_options}
.extraOptions=${extraOptions}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
@@ -69,7 +94,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
.entityId=${this._entityIds}
.attribute=${this.selector.state?.attribute ||
this.context?.filter_attribute}
.extraOptions=${this.selector.state?.extra_options}
.extraOptions=${extraOptions}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
@@ -84,7 +109,8 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
private async _resolveEntityIds(
selectorEntityId: string | string[] | undefined,
contextFilterEntity: string | string[] | undefined,
contextFilterTarget: HassServiceTarget | undefined
contextFilterTarget: HassServiceTarget | undefined,
contextTargetSelector: TargetSelector | undefined
): Promise<string | string[] | undefined> {
if (selectorEntityId !== undefined) {
return selectorEntityId;
@@ -93,8 +119,14 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
return contextFilterEntity;
}
if (contextFilterTarget !== undefined) {
const result = await extractFromTarget(this.hass, contextFilterTarget);
return result.referenced_entities;
return resolveEntityIDs(
this.hass,
contextFilterTarget,
this.hass.entities,
this.hass.devices,
this.hass.areas,
contextTargetSelector
);
}
return undefined;
}

View File

@@ -13,7 +13,6 @@ import { fireEvent } from "../common/dom/fire_event";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import { isIosApp } from "../util/is_ios";
import "./ha-dialog-header";
import "./ha-icon-button";
@@ -185,21 +184,22 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
await this.updateComplete;
requestAnimationFrame(() => {
if (isIosApp(this.hass)) {
const element = this.querySelector("[autofocus]");
if (element !== null) {
if (!element.id) {
element.id = "ha-wa-dialog-autofocus";
}
this.hass.auth.external!.fireMessage({
type: "focus_element",
payload: {
element_id: element.id,
},
});
}
return;
}
// temporary disabled because of issues with focus in iOS app, can be reenabled in 2026.2.0
// if (isIosApp(this.hass)) {
// const element = this.querySelector("[autofocus]");
// if (element !== null) {
// if (!element.id) {
// element.id = "ha-wa-dialog-autofocus";
// }
// this.hass.auth.external!.fireMessage({
// type: "focus_element",
// payload: {
// element_id: element.id,
// },
// });
// }
// return;
// }
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
});
};
@@ -271,6 +271,7 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
}
wa-dialog::part(dialog) {
color: var(--primary-text-color);
min-width: var(--width, var(--full-width));
max-width: var(--width, var(--full-width));
max-height: var(

View File

@@ -16,6 +16,8 @@ export interface RecorderInfo {
export type StatisticType = "change" | "state" | "sum" | "min" | "max" | "mean";
export type StatisticPeriod = "5minute" | "hour" | "day" | "week" | "month";
export type Statistics = Record<string, StatisticValue[]>;
export interface StatisticValue {
@@ -174,7 +176,7 @@ export const fetchStatistics = (
startTime: Date,
endTime?: Date,
statistic_ids?: string[],
period: "5minute" | "hour" | "day" | "week" | "month" = "hour",
period: StatisticPeriod = "hour",
units?: StatisticsUnitConfiguration,
types?: StatisticsTypes
) =>

View File

@@ -929,13 +929,13 @@ export const resolveEntityIDs = (
targetPickerValue: HassServiceTarget,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"]
areas: HomeAssistant["areas"],
targetSelector: TargetSelector = { target: {} }
): string[] => {
if (!targetPickerValue) {
return [];
}
const targetSelector = { target: {} };
const targetEntities = new Set(ensureArray(targetPickerValue.entity_id));
const targetDevices = new Set(ensureArray(targetPickerValue.device_id));
const targetAreas = new Set(ensureArray(targetPickerValue.area_id));

View File

@@ -28,6 +28,7 @@ window.loadES5Adapter = () => {
};
let panelEl: HTMLElement | undefined;
let initialized = false;
function setProperties(properties) {
if (!panelEl) {
@@ -128,13 +129,23 @@ function initialize(
});
}
document.addEventListener(
"DOMContentLoaded",
() => window.parent.customPanel!.registerIframe(initialize, setProperties),
{ once: true }
);
function handleReady() {
if (initialized) return;
initialized = true;
window.parent.customPanel!.registerIframe(initialize, setProperties);
}
window.addEventListener("unload", () => {
// Initial load
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", handleReady, { once: true });
} else {
handleReady();
}
window.addEventListener("pageshow", handleReady);
window.addEventListener("pagehide", () => {
initialized = false;
// allow disconnected callback to fire
while (document.body.lastChild) {
document.body.removeChild(document.body.lastChild);

View File

@@ -59,28 +59,10 @@ export const handleExternalMessage = (
if (msg.command === "restart") {
hassMainEl.hass.connection.reconnect(true);
bus.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else if (msg.command === "navigate") {
navigate(msg.payload.path, msg.payload.options);
bus.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else if (msg.command === "notifications/show") {
fireEvent(hassMainEl, "hass-show-notifications");
bus.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else if (msg.command === "sidebar/toggle") {
if (mainWindow.history.state?.open) {
bus.fireMessage({
@@ -92,12 +74,6 @@ export const handleExternalMessage = (
return true;
}
fireEvent(hassMainEl, "hass-toggle-menu");
bus.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else if (msg.command === "sidebar/show") {
if (mainWindow.history.state?.open) {
bus.fireMessage({
@@ -109,56 +85,29 @@ export const handleExternalMessage = (
return true;
}
fireEvent(hassMainEl, "hass-toggle-menu", { open: true });
bus.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else if (msg.command === "automation/editor/show") {
showAutomationEditor(msg.payload?.config);
bus.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else if (msg.command === "improv/discovered_device") {
fireEvent(window, "improv-discovered-device", msg.payload);
bus.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else if (msg.command === "improv/device_setup_done") {
fireEvent(window, "improv-device-setup-done");
bus.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else if (msg.command === "bar_code/scan_result") {
barCodeListeners.forEach((listener) => listener(msg));
bus.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else if (msg.command === "bar_code/aborted") {
barCodeListeners.forEach((listener) => listener(msg));
bus.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else if (msg.command === "kiosk_mode/set") {
fireEvent(window, "hass-kiosk-mode", { enable: msg.payload.enable });
} else {
return false;
}
bus.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
return true;
};

View File

@@ -301,6 +301,15 @@ export interface EMIncomingMessageImprovDeviceSetupDone extends EMMessage {
command: "improv/device_setup_done";
}
export interface EMIncomingMessageKioskModeSet {
id: number;
type: "command";
command: "kiosk_mode/set";
payload: {
enable: boolean;
};
}
export type EMIncomingMessageCommands =
| EMIncomingMessageRestart
| EMIncomingMessageNavigate
@@ -311,7 +320,8 @@ export type EMIncomingMessageCommands =
| EMIncomingMessageBarCodeScanResult
| EMIncomingMessageBarCodeScanAborted
| EMIncomingMessageImprovDeviceDiscovered
| EMIncomingMessageImprovDeviceSetupDone;
| EMIncomingMessageImprovDeviceSetupDone
| EMIncomingMessageKioskModeSet;
type EMIncomingMessage =
| EMMessageResultSuccess

View File

@@ -282,6 +282,7 @@ export const provideHass = (
dockedSidebar: "auto",
vibrate: true,
debugConnection: false,
kioskMode: false,
suspendWhenHidden: false,
moreInfoEntityId: null as any,
// @ts-ignore

View File

@@ -23,6 +23,8 @@ export interface PageNavigation {
core?: boolean;
advancedOnly?: boolean;
iconPath?: string;
iconSecondaryPath?: string;
iconViewBox?: string;
description?: string;
iconColor?: string;
info?: any;

View File

@@ -44,7 +44,8 @@ export class HomeAssistantMain extends LitElement {
}
protected render(): TemplateResult {
const sidebarNarrow = this._sidebarNarrow || this._externalSidebar;
const sidebarNarrow =
this._sidebarNarrow || this._externalSidebar || this.hass.kioskMode;
const isPanelReady =
this.hass.panels && this.hass.userData && this.hass.systemData;
@@ -133,7 +134,7 @@ export class HomeAssistantMain extends LitElement {
toggleAttribute(
this,
"modal",
this._sidebarNarrow || this._externalSidebar
this._sidebarNarrow || this._externalSidebar || this.hass.kioskMode
);
}

View File

@@ -50,7 +50,6 @@ import {
import "../../../layouts/hass-tabs-subpage";
import type { HomeAssistant, Route } from "../../../types";
import { showToast } from "../../../util/toast";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
import {
loadAreaRegistryDetailDialog,

View File

@@ -4,7 +4,6 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import "../../../../../components/ha-checkbox";
import "../../../../../components/ha-selector/ha-selector";
import "../../../../../components/ha-settings-row";
@@ -269,8 +268,11 @@ export class HaPlatformCondition extends LitElement {
return undefined;
}
const context = {};
const context: Record<string, any> = {};
for (const [context_key, data_key] of Object.entries(field.context)) {
if (data_key === "target" && this.description?.target) {
context.target_selector = this._targetSelector(this.description.target);
}
context[context_key] =
data_key === "target"
? this.condition.target
@@ -378,7 +380,7 @@ export class HaPlatformCondition extends LitElement {
return "";
}
return this.hass.localize(
`component.${computeDomain(this.condition.condition)}.selector.${key}`
`component.${getConditionDomain(this.condition.condition)}.selector.${key}`
);
};

View File

@@ -82,7 +82,6 @@ import type { Entries, HomeAssistant, Route } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import "../ha-config-section";
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
import {
type EntityRegistryUpdate,

View File

@@ -1,10 +1,16 @@
import { consume } from "@lit/context";
import { mdiAlert, mdiFormatListBulleted, mdiShape } from "@mdi/js";
import {
mdiAlert,
mdiCodeBraces,
mdiFormatListBulleted,
mdiShape,
} from "@mdi/js";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { css, html, LitElement, type nothing, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ensureArray } from "../../../../common/array/ensure-array";
import { transform } from "../../../../common/decorators/transform";
import { isTemplate } from "../../../../common/string/has-template";
import "../../../../components/ha-svg-icon";
import type { ConfigEntry } from "../../../../data/config_entries";
import {
@@ -167,6 +173,16 @@ export class HaAutomationRowTargets extends LitElement {
);
}
// Check if the target is a template
if (isTemplate(targetId)) {
return this._renderTargetBadge(
html`<ha-svg-icon .path=${mdiCodeBraces}></ha-svg-icon>`,
this.localize(
"ui.panel.config.automation.editor.target_summary.template"
)
);
}
const exists = this._checkTargetExists(targetType, targetId);
if (!exists) {
return this._renderTargetBadge(

View File

@@ -4,7 +4,6 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import "../../../../../components/ha-checkbox";
import "../../../../../components/ha-selector/ha-selector";
import "../../../../../components/ha-settings-row";
@@ -305,8 +304,11 @@ export class HaPlatformTrigger extends LitElement {
return undefined;
}
const context = {};
const context: Record<string, any> = {};
for (const [context_key, data_key] of Object.entries(field.context)) {
if (data_key === "target" && this.description?.target) {
context.target_selector = this._targetSelector(this.description.target);
}
context[context_key] =
data_key === "target"
? this.trigger.target
@@ -414,7 +416,7 @@ export class HaPlatformTrigger extends LitElement {
return "";
}
return this.hass.localize(
`component.${computeDomain(this.trigger.trigger)}.selector.${key}`
`component.${getTriggerDomain(this.trigger.trigger)}.selector.${key}`
);
};

View File

@@ -163,7 +163,7 @@ class DialogImportBlueprint extends LitElement {
</div>
<ha-button
appearance="plain"
slot="primaryAction"
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._saving}
>

View File

@@ -8,7 +8,7 @@ import {
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
@@ -39,7 +39,6 @@ import {
} from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@@ -155,8 +154,6 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public cloudStatus?: CloudStatus;
@property({ attribute: false }) public showAdvanced = false;
@state() private _tip?: string;
@state() private _repairsIssues: { issues: RepairsIssue[]; total: number } = {
@@ -164,21 +161,24 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
total: 0,
};
private _pages = memoizeOne((cloudStatus, isCloudLoaded) => {
const pages: PageNavigation[] = [];
if (isCloudLoaded) {
pages.push({
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
translationKey: "cloud",
});
}
return [...pages, ...configSections.dashboard];
});
private _pages = memoizeOne((cloudStatus, isCloudLoaded) => [
isCloudLoaded
? [
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
translationKey: "cloud",
},
...configSections.dashboard,
]
: configSections.dashboard,
configSections.dashboard_2,
configSections.dashboard_3,
]);
public hassSubscribe(): UnsubscribeFunc[] {
return [
@@ -308,18 +308,22 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
: ""}
</ha-card>`
: ""}
<ha-card outlined>
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${this._pages(
this.cloudStatus,
isComponentLoaded(this.hass, "cloud")
)}
></ha-config-navigation>
</ha-card>
${this._pages(
this.cloudStatus,
isComponentLoaded(this.hass, "cloud")
).map((categoryPages) =>
categoryPages.length === 0
? nothing
: html`
<ha-card outlined>
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.pages=${categoryPages}
></ha-config-navigation>
</ha-card>
`
)}
<ha-tip .hass=${this.hass}>${this._tip}</ha-tip>
</ha-config-section>
</ha-top-app-bar-fixed>

View File

@@ -1,11 +1,12 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { canShowPage } from "../../../common/config/can_show_page";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import "../../../components/ha-navigation-list";
import type { CloudStatus } from "../../../data/cloud";
import { getConfigEntries } from "../../../data/config_entries";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../../../types";
@@ -17,13 +18,29 @@ class HaConfigNavigation extends LitElement {
@property({ attribute: false }) public pages!: PageNavigation[];
@state() private _hasBluetoothConfigEntries = false;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
getConfigEntries(this.hass, {
domain: "bluetooth",
}).then((bluetoothEntries) => {
this._hasBluetoothConfigEntries = bluetoothEntries.length > 0;
});
}
protected render(): TemplateResult {
const pages = this.pages
.filter((page) =>
page.path === "#external-app-configuration"
? this.hass.auth.external?.config.hasSettingsScreen
: canShowPage(this.hass, page)
)
.filter((page) => {
if (page.path === "#external-app-configuration") {
return this.hass.auth.external?.config.hasSettingsScreen;
}
// Only show Bluetooth page if there are Bluetooth config entries
if (page.component === "bluetooth") {
return this._hasBluetoothConfigEntries;
}
return canShowPage(this.hass, page);
})
.map((page) => ({
...page,
name:

View File

@@ -5,8 +5,9 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
import "../../../../components/ha-alert";
import "../../../../components/ha-area-picker";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-labels-picker";
import type { HaSwitch } from "../../../../components/ha-switch";
import "../../../../components/ha-textfield";
@@ -19,6 +20,8 @@ import type { DeviceRegistryDetailDialogParams } from "./show-dialog-device-regi
class DialogDeviceRegistryDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@state() private _nameByUser!: string;
@state() private _error?: string;
@@ -42,10 +45,15 @@ class DialogDeviceRegistryDetail extends LitElement {
this._areaId = this._params.device.area_id || "";
this._labels = this._params.device.labels || [];
this._disabledBy = this._params.device.disabled_by;
this._open = true;
await this.updateComplete;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._error = "";
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -57,10 +65,12 @@ class DialogDeviceRegistryDetail extends LitElement {
}
const device = this._params.device;
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${computeDeviceNameDisplay(device, this.hass)}
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${computeDeviceNameDisplay(device, this.hass)}
prevent-scrim-close
@closed=${this._dialogClosed}
>
<div>
${this._error
@@ -68,6 +78,7 @@ class DialogDeviceRegistryDetail extends LitElement {
: ""}
<div class="form">
<ha-textfield
autofocus
.value=${this._nameByUser}
@input=${this._nameChanged}
.label=${this.hass.localize(
@@ -75,7 +86,6 @@ class DialogDeviceRegistryDetail extends LitElement {
)}
.placeholder=${device.name || ""}
.disabled=${this._submitting}
dialogInitialFocus
></ha-textfield>
<ha-area-picker
.hass=${this.hass}
@@ -131,22 +141,25 @@ class DialogDeviceRegistryDetail extends LitElement {
</div>
</div>
</div>
<ha-button
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
appearance="plain"
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${this._submitting}
>
${this.hass.localize("ui.dialogs.device-registry-detail.update")}
</ha-button>
</ha-dialog>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
appearance="plain"
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${this._submitting}
>
${this.hass.localize("ui.dialogs.device-registry-detail.update")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}

View File

@@ -3,6 +3,7 @@ import {
mdiAccount,
mdiBackupRestore,
mdiBadgeAccountHorizontal,
mdiBluetooth,
mdiCellphoneCog,
mdiCog,
mdiDatabase,
@@ -29,6 +30,8 @@ import {
mdiTools,
mdiUpdate,
mdiViewDashboard,
mdiZigbee,
mdiZWave,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
@@ -101,6 +104,59 @@ export const configSections: Record<string, PageNavigation[]> = {
iconPath: mdiMicrophone,
iconColor: "#3263C3",
},
],
dashboard_2: [
{
path: "/config/zha",
name: "Zigbee",
iconPath: mdiZigbee,
iconColor: "#E74011",
component: "zha",
translationKey: "zha",
},
{
path: "/config/zwave_js",
name: "Z-Wave",
iconPath: mdiZWave,
iconColor: "#153163",
component: "zwave_js",
translationKey: "zwave_js",
},
{
path: "/knx",
name: "KNX",
iconPath:
"M 3.9861338,14.261456 3.7267552,13.934877 6.3179131,11.306266 H 4.466374 l -2.6385205,2.68258 V 11.312882 H 0.00440574 L 0,17.679803 l 1.8278535,5.43e-4 v -1.818482 l 0.7225444,-0.732459 2.1373588,2.543782 2.1869324,-5.44e-4 M 24,17.680369 21.809238,17.669359 19.885559,15.375598 17.640262,17.68037 h -1.828407 l 3.236048,-3.302138 -2.574075,-3.067547 2.135161,0.0016 1.610309,1.87687 1.866403,-1.87687 h 1.828429 l -2.857742,2.87478 m -10.589867,-2.924898 2.829625,3.990552 -0.01489,-3.977887 1.811889,-0.0044 0.0011,6.357564 -2.093314,-5.44e-4 -2.922133,-3.947594 -0.0314,3.947594 H 8.2581097 V 11.261677 M 11.971203,6.3517488 c 0,0 2.800714,-0.093203 6.172001,1.0812045 3.462393,1.0898845 5.770926,3.4695627 5.770926,3.4695627 l -1.823898,-5.43e-4 C 22.088532,10.900273 20.577938,9.4271528 17.660223,8.5024618 15.139256,7.703366 12.723057,7.645835 12.111178,7.6449876 l -9.71e-4,0.0011 c 0,0 -0.0259,-6.4e-4 -0.07527,-9.714e-4 -0.04726,3.33e-4 -0.07201,9.714e-4 -0.07201,9.714e-4 v -0.00113 C 11.337007,7.6453728 8.8132091,7.7001736 6.2821829,8.5024618 3.3627914,9.4276738 1.8521646,10.901973 1.8521646,10.901973 l -1.82398708,5.43e-4 C 0.03128403,10.899322 2.339143,8.5221038 5.799224,7.4329533 9.170444,6.2585642 11.971203,6.3517488 11.971203,6.3517488 Z",
iconColor: "#4EAA66",
component: "knx",
translationKey: "knx",
},
{
path: "/config/thread",
name: "Thread",
iconPath:
"m 17.126982,8.0730792 c 0,-0.7297242 -0.593746,-1.32357 -1.323637,-1.32357 -0.729454,0 -1.323199,0.5938458 -1.323199,1.32357 v 1.3234242 l 1.323199,1.458e-4 c 0.729891,0 1.323637,-0.5937006 1.323637,-1.32357 z M 11.999709,0 C 5.3829818,0 0,5.3838955 0,12.001455 0,18.574352 5.3105455,23.927406 11.865164,24 V 12.012075 l -3.9275642,-2.91e-4 c -1.1669814,0 -2.1169453,0.949979 -2.1169453,2.118323 0,1.16718 0.9499639,2.116868 2.1169453,2.116868 v 2.615717 c -2.6093089,0 -4.732218,-2.12327 -4.732218,-4.732585 0,-2.61048 2.1229091,-4.7343308 4.732218,-4.7343308 l 3.9275642,5.82e-4 v -1.323279 c 0,-2.172296 1.766691,-3.9395777 3.938181,-3.9395777 2.171928,0 3.9392,1.7672817 3.9392,3.9395777 0,2.1721498 -1.767272,3.9395768 -3.9392,3.9395768 l -1.323199,-1.45e-4 V 23.744102 C 19.911127,22.597726 24,17.768833 24,12.001455 24,5.3838955 18.616727,0 11.999709,0 Z",
iconColor: "#ED7744",
component: "thread",
translationKey: "thread",
},
{
path: "/config/bluetooth",
name: "Bluetooth",
iconPath: mdiBluetooth,
iconColor: "#0082FC",
component: "bluetooth",
translationKey: "bluetooth",
},
{
path: "/insteon",
name: "Insteon",
iconPath:
"m 12.001571,6.3842473 h 0.02973 c 3.652189,0 6.767389,-2.29456 7.987462,-5.5177193 L 15.389382,0 Z m 0,0 h -0.02972 c -3.6522186,0 -6.7673314,-2.2918546 -7.9874477,-5.5177193 h -0.00271 L 8.6111273,0 Z M 6.3840436,11.999287 v -0.02972 c 0,-3.6524074 -2.2944727,-6.7675928 -5.51754469,-7.9877383 L 0,8.6114473 Z m 0,0 v 0.02964 c 0,3.652378 -2.2917818,6.767578 -5.51754469,7.987796 v 0.0026 L 0,15.389818 Z M 24,8.6114473 23.133527,3.9818327 v 0.00269 C 19.907636,5.2046836 17.616,8.3198691 17.616,11.972276 v 0.02966 0.02972 0.0027 c 0,3.65232 2.2944,6.76752 5.517527,7.987738 L 24,15.392436 17.616,12.001935 Z M 20.018618,23.133527 15.389091,24 11.99872,17.615709 h 0.02964 c 3.652218,0 6.767418,2.291927 7.987491,5.517818 z M 11.99872,17.615709 8.6082618,24 3.9788364,23.133527 C 5.1989527,19.9104 8.3140655,17.615709 11.966284,17.615709 h 0.0027 z",
iconColor: "#E4002C",
component: "insteon",
translationKey: "insteon",
},
{
path: "/config/tags",
translationKey: "tags",
@@ -108,6 +164,8 @@ export const configSections: Record<string, PageNavigation[]> = {
iconColor: "#616161",
component: "tag",
},
],
dashboard_3: [
{
path: "/config/person",
translationKey: "people",

View File

@@ -23,23 +23,12 @@ import {
subscribeBluetoothScannersDetails,
} from "../../../../../data/bluetooth";
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import "../../../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import { bluetoothTabs } from "./bluetooth-config-dashboard";
import { showBluetoothDeviceInfoDialog } from "./show-dialog-bluetooth-device-info";
export const bluetoothAdvertisementMonitorTabs: PageNavigation[] = [
{
translationKey: "ui.panel.config.bluetooth.advertisement_monitor",
path: "advertisement-monitor",
},
{
translationKey: "ui.panel.config.bluetooth.visualization",
path: "visualization",
},
];
@customElement("bluetooth-advertisement-monitor")
export class BluetoothAdvertisementMonitorPanel extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -232,7 +221,7 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
@collapsed-changed=${this._handleCollapseChanged}
filter=${this.address || ""}
clickable
.tabs=${bluetoothAdvertisementMonitorTabs}
.tabs=${bluetoothTabs}
></hass-tabs-subpage-data-table>
`;
}

View File

@@ -1,21 +1,19 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-card";
import "../../../../../components/ha-code-editor";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-switch";
import "../../../../../components/ha-button";
import { getConfigEntries } from "../../../../../data/config_entries";
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import {
subscribeBluetoothConnectionAllocations,
subscribeBluetoothScannerState,
subscribeBluetoothScannersDetails,
} from "../../../../../data/bluetooth";
mdiBroadcast,
mdiCogOutline,
mdiLan,
mdiLinkVariant,
mdiNetwork,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-list";
import "../../../../../components/ha-list-item";
import type {
BluetoothAllocationsData,
BluetoothScannerState,
@@ -23,29 +21,60 @@ import type {
HaScannerType,
} from "../../../../../data/bluetooth";
import {
getValueInPercentage,
roundWithOneDecimal,
} from "../../../../../util/calculate";
import "../../../../../components/ha-metric";
subscribeBluetoothConnectionAllocations,
subscribeBluetoothScannerState,
subscribeBluetoothScannersDetails,
} from "../../../../../data/bluetooth";
import type { ConfigEntry } from "../../../../../data/config_entries";
import { getConfigEntries } from "../../../../../data/config_entries";
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
import "../../../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
export const bluetoothTabs: PageNavigation[] = [
{
translationKey: "ui.panel.config.bluetooth.tabs.overview",
path: `/config/bluetooth/dashboard`,
iconPath: mdiNetwork,
},
{
translationKey: "ui.panel.config.bluetooth.tabs.advertisements",
path: `/config/bluetooth/advertisement-monitor`,
iconPath: mdiBroadcast,
},
{
translationKey: "ui.panel.config.bluetooth.tabs.visualization",
path: `/config/bluetooth/visualization`,
iconPath: mdiLan,
},
{
translationKey: "ui.panel.config.bluetooth.tabs.connections",
path: `/config/bluetooth/connection-monitor`,
iconPath: mdiLinkVariant,
},
];
@customElement("bluetooth-config-dashboard")
export class BluetoothConfigDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@state() private _configEntries: ConfigEntry[] = [];
@state() private _connectionAllocationData: BluetoothAllocationsData[] = [];
@state() private _connectionAllocationsError?: string;
@state() private _scannerState?: BluetoothScannerState;
@state() private _scannerStates: Record<string, BluetoothScannerState> = {};
@state() private _scannerDetails?: BluetoothScannersDetails;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
private _unsubConnectionAllocations?: (() => Promise<void>) | undefined;
private _unsubScannerState?: (() => Promise<void>) | undefined;
@@ -55,41 +84,44 @@ export class BluetoothConfigDashboard extends LitElement {
public connectedCallback(): void {
super.connectedCallback();
if (this.hass) {
this._loadConfigEntries();
this._subscribeBluetoothConnectionAllocations();
this._subscribeBluetoothScannerState();
this._subscribeScannerDetails();
}
}
private async _loadConfigEntries(): Promise<void> {
this._configEntries = await getConfigEntries(this.hass, {
domain: "bluetooth",
});
}
private async _subscribeBluetoothConnectionAllocations(): Promise<void> {
if (this._unsubConnectionAllocations || !this._configEntry) {
if (this._unsubConnectionAllocations) {
return;
}
try {
this._unsubConnectionAllocations =
await subscribeBluetoothConnectionAllocations(
this.hass.connection,
(data) => {
this._connectionAllocationData = data;
},
this._configEntry
);
} catch (err: any) {
this._unsubConnectionAllocations = undefined;
this._connectionAllocationsError = err.message;
}
this._unsubConnectionAllocations =
await subscribeBluetoothConnectionAllocations(
this.hass.connection,
(data) => {
this._connectionAllocationData = data;
}
);
}
private async _subscribeBluetoothScannerState(): Promise<void> {
if (this._unsubScannerState || !this._configEntry) {
if (this._unsubScannerState) {
return;
}
this._unsubScannerState = await subscribeBluetoothScannerState(
this.hass.connection,
(scannerState) => {
this._scannerState = scannerState;
},
this._configEntry
this._scannerStates = {
...this._scannerStates,
[scannerState.source]: scannerState,
};
}
);
}
@@ -122,92 +154,111 @@ export class BluetoothConfigDashboard extends LitElement {
}
protected render(): TemplateResult {
// Get scanner type to determine if options button should be shown
const scannerDetails =
this._scannerState && this._scannerDetails?.[this._scannerState.source];
const scannerType: HaScannerType =
scannerDetails?.scanner_type ?? "unknown";
const isRemoteScanner = scannerType === "remote";
return html`
<hass-subpage .narrow=${this.narrow} .hass=${this.hass}>
<hass-tabs-subpage
header=${this.hass.localize("ui.panel.config.bluetooth.title")}
.narrow=${this.narrow}
.hass=${this.hass}
.route=${this.route}
.tabs=${bluetoothTabs}
>
<div class="content">
<ha-card
.header=${this.hass.localize(
"ui.panel.config.bluetooth.settings_title"
)}
>
<div class="card-content">${this._renderScannerState()}</div>
${!isRemoteScanner
? html`<div class="card-actions">
<ha-button @click=${this._openOptionFlow}
>${this.hass.localize(
"ui.panel.config.bluetooth.option_flow"
)}</ha-button
>
</div>`
: nothing}
</ha-card>
<ha-card
.header=${this.hass.localize(
"ui.panel.config.bluetooth.advertisement_monitor"
)}
>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.bluetooth.advertisement_monitor_details"
)}
</p>
</div>
<div class="card-actions">
<ha-button
href="/config/bluetooth/advertisement-monitor"
appearance="plain"
>
${this.hass.localize(
"ui.panel.config.bluetooth.advertisement_monitor"
)}
</ha-button>
<ha-button
href="/config/bluetooth/visualization"
appearance="plain"
>
${this.hass.localize("ui.panel.config.bluetooth.visualization")}
</ha-button>
</div>
</ha-card>
<ha-card
.header=${this.hass.localize(
"ui.panel.config.bluetooth.connection_slot_allocations_monitor"
)}
>
<div class="card-content">
${this._renderConnectionAllocations()}
</div>
<div class="card-actions">
<ha-button
href="/config/bluetooth/connection-monitor"
appearance="plain"
>
${this.hass.localize(
"ui.panel.config.bluetooth.connection_monitor"
)}
</ha-button>
</div>
<ha-list>${this._renderAdaptersList()}</ha-list>
</ha-card>
</div>
</hass-subpage>
</hass-tabs-subpage>
`;
}
private _getUsedAllocations = (used: number, total: number) =>
roundWithOneDecimal(getValueInPercentage(used, 0, total));
private _renderAdaptersList() {
if (this._configEntries.length === 0) {
return html`<ha-list-item noninteractive>
${this.hass.localize(
"ui.panel.config.bluetooth.no_scanner_state_available"
)}
</ha-list-item>`;
}
// Build source to device mapping (same as visualization)
const sourceDevices: Record<string, DeviceRegistryEntry> = {};
Object.values(this.hass.devices).forEach((device) => {
const btConnection = device.connections.find(
(connection) => connection[0] === "bluetooth"
);
if (btConnection) {
sourceDevices[btConnection[1]] = device;
}
});
return this._configEntries.map((entry) => {
// Find scanner by matching device's config_entries to this entry
const scannerDetails = this._scannerDetails
? Object.values(this._scannerDetails).find((d) => {
const device = sourceDevices[d.source];
return device?.config_entries.includes(entry.entry_id);
})
: undefined;
const scannerState = scannerDetails
? this._scannerStates[scannerDetails.source]
: undefined;
const scannerType: HaScannerType =
scannerDetails?.scanner_type ?? "unknown";
const isRemoteScanner = scannerType === "remote";
const hasMismatch =
scannerState &&
scannerState.current_mode !== scannerState.requested_mode;
// Find allocation data for this scanner
const allocations = scannerDetails
? this._connectionAllocationData.find(
(a) => a.source === scannerDetails.source
)
: undefined;
const secondaryText = this._formatScannerModeText(scannerState);
return html`
<ha-list-item twoline hasMeta noninteractive>
<span>${entry.title}</span>
<span slot="secondary">
${secondaryText}${allocations
? allocations.slots > 0
? ` · ${allocations.slots - allocations.free}/${allocations.slots} ${this.hass.localize("ui.panel.config.bluetooth.active_connections")}`
: ` · ${this.hass.localize("ui.panel.config.bluetooth.no_connection_slots")}`
: nothing}
</span>
${!isRemoteScanner
? html`<ha-icon-button
slot="meta"
.path=${mdiCogOutline}
.entry=${entry}
@click=${this._openOptionFlow}
.label=${this.hass.localize(
"ui.panel.config.bluetooth.option_flow"
)}
></ha-icon-button>`
: nothing}
</ha-list-item>
${hasMismatch && scannerDetails
? this._renderScannerMismatchWarning(
entry.title,
scannerState,
scannerType
)
: nothing}
`;
});
}
private _renderScannerMismatchWarning(
name: string,
scannerState: BluetoothScannerState,
scannerType: HaScannerType,
formatMode: (mode: string | null) => string
scannerType: HaScannerType
) {
const instructions: string[] = [];
@@ -238,8 +289,9 @@ export class BluetoothConfigDashboard extends LitElement {
${this.hass.localize(
"ui.panel.config.bluetooth.scanner_mode_mismatch",
{
requested: formatMode(scannerState.requested_mode),
current: formatMode(scannerState.current_mode),
name: name,
requested: this._formatMode(scannerState.requested_mode),
current: this._formatMode(scannerState.current_mode),
}
)}
</div>
@@ -249,127 +301,59 @@ export class BluetoothConfigDashboard extends LitElement {
</ha-alert>`;
}
private _renderScannerState() {
if (!this._configEntry || !this._scannerState) {
return html`<div>
${this.hass.localize(
"ui.panel.config.bluetooth.no_scanner_state_available"
)}
</div>`;
private _formatMode(mode: string | null): string {
switch (mode) {
case null:
return this.hass.localize(
"ui.panel.config.bluetooth.scanning_mode_none"
);
case "active":
return this.hass.localize(
"ui.panel.config.bluetooth.scanning_mode_active"
);
case "passive":
return this.hass.localize(
"ui.panel.config.bluetooth.scanning_mode_passive"
);
default:
return mode;
}
const scannerState = this._scannerState;
// Find the scanner details for this source
const scannerDetails = this._scannerDetails?.[scannerState.source];
const scannerType: HaScannerType =
scannerDetails?.scanner_type ?? "unknown";
const formatMode = (mode: string | null) => {
switch (mode) {
case null:
return this.hass.localize(
"ui.panel.config.bluetooth.scanning_mode_none"
);
case "active":
return this.hass.localize(
"ui.panel.config.bluetooth.scanning_mode_active"
);
case "passive":
return this.hass.localize(
"ui.panel.config.bluetooth.scanning_mode_passive"
);
default:
return mode; // Fallback for unknown modes
}
};
return html`
<div class="scanner-state">
<div class="state-row">
<span
>${this.hass.localize(
"ui.panel.config.bluetooth.current_scanning_mode"
)}:</span
>
<span class="state-value"
>${formatMode(scannerState.current_mode)}</span
>
</div>
<div class="state-row">
<span
>${this.hass.localize(
"ui.panel.config.bluetooth.requested_scanning_mode"
)}:</span
>
<span class="state-value"
>${formatMode(scannerState.requested_mode)}</span
>
</div>
${scannerState.current_mode !== scannerState.requested_mode
? this._renderScannerMismatchWarning(
scannerState,
scannerType,
formatMode
)
: nothing}
</div>
`;
}
private _renderConnectionAllocations() {
if (this._connectionAllocationsError) {
return html`<ha-alert alert-type="error"
>${this._connectionAllocationsError}</ha-alert
>`;
private _formatModeLabel(mode: string | null): string {
switch (mode) {
case null:
return this.hass.localize(
"ui.panel.config.bluetooth.scanning_mode_none_label"
);
case "active":
return this.hass.localize(
"ui.panel.config.bluetooth.scanning_mode_active_label"
);
case "passive":
return this.hass.localize(
"ui.panel.config.bluetooth.scanning_mode_passive_label"
);
default:
return mode;
}
if (this._connectionAllocationData.length === 0) {
return html`<div>
${this.hass.localize(
"ui.panel.config.bluetooth.no_connection_slot_allocations"
)}
</div>`;
}
const allocations = this._connectionAllocationData[0];
const allocationsUsed = allocations.slots - allocations.free;
const allocationsTotal = allocations.slots;
if (allocationsTotal === 0) {
return html`<div>
${this.hass.localize(
"ui.panel.config.bluetooth.no_active_connection_support"
)}
</div>`;
}
return html`
<p>
${this.hass.localize(
"ui.panel.config.bluetooth.connection_slot_allocations_monitor_details",
{ slots: allocationsTotal }
)}
</p>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.bluetooth.used_connection_slot_allocations"
)}
.value=${this._getUsedAllocations(allocationsUsed, allocationsTotal)}
.tooltip=${allocations.allocated.length > 0
? `${allocationsUsed}/${allocationsTotal} (${allocations.allocated.join(", ")})`
: `${allocationsUsed}/${allocationsTotal}`}
></ha-metric>
`;
}
private async _openOptionFlow() {
const configEntryId = this._configEntry;
if (!configEntryId) {
return;
private _formatScannerModeText(
scannerState: BluetoothScannerState | undefined
): string {
if (!scannerState) {
return this.hass.localize(
"ui.panel.config.bluetooth.scanner_state_unknown"
);
}
const configEntries = await getConfigEntries(this.hass, {
domain: "bluetooth",
});
const configEntry = configEntries.find(
(entry) => entry.entry_id === configEntryId
);
showOptionsFlowDialog(this, configEntry!);
return this._formatModeLabel(scannerState.current_mode);
}
private _openOptionFlow(ev: Event) {
const button = ev.currentTarget as HTMLElement & { entry: ConfigEntry };
showOptionsFlowDialog(this, button.entry);
}
static get styles(): CSSResultGroup {
@@ -394,17 +378,9 @@ export class BluetoothConfigDashboard extends LitElement {
display: flex;
justify-content: flex-end;
}
.scanner-state {
margin-bottom: 16px;
}
.state-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
}
.state-value {
font-weight: 500;
ha-list-item {
--mdc-list-item-meta-display: flex;
--mdc-list-item-meta-size: 48px;
}
`,
];

View File

@@ -24,6 +24,7 @@ import type { DeviceRegistryEntry } from "../../../../../data/device/device_regi
import "../../../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import { bluetoothTabs } from "./bluetooth-config-dashboard";
@customElement("bluetooth-connection-monitor")
export class BluetoothConnectionMonitorPanel extends LitElement {
@@ -214,6 +215,7 @@ export class BluetoothConnectionMonitorPanel extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${bluetoothTabs}
.columns=${this._columns(this.hass.localize)}
.data=${this._dataWithNamedSourceAndIds(this._data)}
.initialGroupColumn=${this._activeGrouping}

View File

@@ -26,9 +26,9 @@ import {
subscribeBluetoothScannersDetails,
} from "../../../../../data/bluetooth";
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
import "../../../../../layouts/hass-subpage";
import "../../../../../layouts/hass-tabs-subpage";
import type { HomeAssistant, Route } from "../../../../../types";
import { bluetoothAdvertisementMonitorTabs } from "./bluetooth-advertisement-monitor";
import { bluetoothTabs } from "./bluetooth-config-dashboard";
const UPDATE_THROTTLE_TIME = 10000;
@@ -123,8 +123,7 @@ export class BluetoothNetworkVisualization extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
header=${this.hass.localize("ui.panel.config.bluetooth.visualization")}
.tabs=${bluetoothAdvertisementMonitorTabs}
.tabs=${bluetoothTabs}
>
<ha-network-graph
.hass=${this.hass}

View File

@@ -46,44 +46,54 @@ export class MatterConfigDashboard extends LitElement {
href="/config/thread"
slot="toolbar-icon"
>
Visit Thread Panel</ha-button
${this.hass.localize(
"ui.panel.config.matter.panel.thread_panel"
)}</ha-button
>
`
: ""}
<div class="content">
<ha-card header="Matter">
<ha-alert alert-type="warning"
>Matter is still in the early phase of development, it is not
meant to be used in production. This panel is for development
only.</ha-alert
>${this.hass.localize(
"ui.panel.config.matter.panel.experimental_note"
)}</ha-alert
>
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
You can add Matter devices by commissing them if they are not
setup yet, or share them from another controller and enter the
share code.
${this.hass.localize("ui.panel.config.matter.panel.add_devices")}
</div>
<div class="card-actions">
${canCommissionMatterExternal(this.hass)
? html`<ha-button
appearance="plain"
@click=${this._startMobileCommissioning}
>Commission device with mobile app</ha-button
>${this.hass.localize(
"ui.panel.config.matter.panel.mobile_app_commisioning"
)}</ha-button
>`
: ""}
<ha-button appearance="plain" @click=${this._commission}
>Commission device</ha-button
>${this.hass.localize(
"ui.panel.config.matter.panel.commission_device"
)}</ha-button
>
<ha-button appearance="plain" @click=${this._acceptSharedDevice}
>Add shared device</ha-button
>${this.hass.localize(
"ui.panel.config.matter.panel.add_shared_device"
)}</ha-button
>
<ha-button appearance="plain" @click=${this._setWifi}
>Set WiFi Credentials</ha-button
>${this.hass.localize(
"ui.panel.config.matter.panel.set_wifi_credentials"
)}</ha-button
>
<ha-button appearance="plain" @click=${this._setThread}
>Set Thread Credentials</ha-button
>${this.hass.localize(
"ui.panel.config.matter.panel.set_thread_credentials"
)}</ha-button
>
</div>
</ha-card>
@@ -114,19 +124,31 @@ export class MatterConfigDashboard extends LitElement {
private async _setWifi(): Promise<void> {
this._error = undefined;
const networkName = await showPromptDialog(this, {
title: "Network name",
inputLabel: "Network name",
title: this.hass.localize(
"ui.panel.config.matter.panel.prompts.network_name.title"
),
inputLabel: this.hass.localize(
"ui.panel.config.matter.panel.prompts.network_name.input_label"
),
inputType: "string",
confirmText: "Continue",
confirmText: this.hass.localize(
"ui.panel.config.matter.panel.prompts.network_name.confirm"
),
});
if (!networkName) {
return;
}
const psk = await showPromptDialog(this, {
title: "Passcode",
inputLabel: "Code",
title: this.hass.localize(
"ui.panel.config.matter.panel.prompts.passcode.title"
),
inputLabel: this.hass.localize(
"ui.panel.config.matter.panel.prompts.passcode.input_label"
),
inputType: "password",
confirmText: "Set Wifi",
confirmText: this.hass.localize(
"ui.panel.config.matter.panel.prompts.passcode.confirm"
),
});
if (!psk) {
return;
@@ -140,10 +162,16 @@ export class MatterConfigDashboard extends LitElement {
private async _commission(): Promise<void> {
const code = await showPromptDialog(this, {
title: "Commission device",
inputLabel: "Code",
title: this.hass.localize(
"ui.panel.config.matter.panel.prompts.commission_device.title"
),
inputLabel: this.hass.localize(
"ui.panel.config.matter.panel.prompts.commission_device.input_label"
),
inputType: "string",
confirmText: "Commission",
confirmText: this.hass.localize(
"ui.panel.config.matter.panel.prompts.commission_device.confirm"
),
});
if (!code) {
return;
@@ -160,10 +188,16 @@ export class MatterConfigDashboard extends LitElement {
private async _acceptSharedDevice(): Promise<void> {
const code = await showPromptDialog(this, {
title: "Add shared device",
inputLabel: "Pin",
title: this.hass.localize(
"ui.panel.config.matter.panel.prompts.add_shared_device.title"
),
inputLabel: this.hass.localize(
"ui.panel.config.matter.panel.prompts.add_shared_device.input_label"
),
inputType: "number",
confirmText: "Accept device",
confirmText: this.hass.localize(
"ui.panel.config.matter.panel.prompts.add_shared_device.confirm"
),
});
if (!code) {
return;
@@ -180,10 +214,16 @@ export class MatterConfigDashboard extends LitElement {
private async _setThread(): Promise<void> {
const code = await showPromptDialog(this, {
title: "Set Thread operation",
inputLabel: "Dataset",
title: this.hass.localize(
"ui.panel.config.matter.panel.prompts.set_thread.title"
),
inputLabel: this.hass.localize(
"ui.panel.config.matter.panel.prompts.set_thread.input_label"
),
inputType: "string",
confirmText: "Set Thread",
confirmText: this.hass.localize(
"ui.panel.config.matter.panel.prompts.set_thread.confirm"
),
});
if (!code) {
return;

View File

@@ -1,5 +1,4 @@
import { customElement, property } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
import type { RouterOptions } from "../../../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../../../layouts/hass-router-page";
import type { HomeAssistant } from "../../../../../types";
@@ -12,10 +11,6 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
@property({ type: Boolean }) public narrow = false;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
showLoading: true,
@@ -52,7 +47,6 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
el.hass = this.hass;
el.isWide = this.isWide;
el.narrow = this.narrow;
el.configEntryId = this._configEntry;
if (this._currentPage === "group") {
el.groupId = this.routeTail.path.substr(1);
} else if (this._currentPage === "device") {
@@ -60,17 +54,6 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
} else if (this._currentPage === "visualization") {
el.zoomedDeviceIdFromURL = this.routeTail.path.substr(1);
}
const searchParams = new URLSearchParams(window.location.search);
if (this._configEntry && !searchParams.has("config_entry")) {
searchParams.append("config_entry", this._configEntry);
navigate(
`${this.routeTail.prefix}${
this.routeTail.path
}?${searchParams.toString()}`,
{ replace: true }
);
}
}
}

View File

@@ -75,7 +75,7 @@ class ZHAConfigDashboard extends LitElement {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public configEntryId?: string;
@state() private _configEntry?: ConfigEntry;
@state() private _configuration?: ZHAConfiguration;
@@ -95,6 +95,7 @@ class ZHAConfigDashboard extends LitElement {
super.firstUpdated(changedProperties);
if (this.hass) {
this.hass.loadBackendTranslation("config_panel", "zha", false);
this._fetchConfigEntry();
this._fetchConfiguration();
this._fetchSettings();
this._fetchDevicesAndUpdateStatus();
@@ -110,7 +111,6 @@ class ZHAConfigDashboard extends LitElement {
.narrow=${this.narrow}
.route=${this.route}
.tabs=${zhaTabs}
back-path="/config/integrations"
has-fab
>
<div class="container">
@@ -151,28 +151,26 @@ class ZHAConfigDashboard extends LitElement {
</div>
</div>
</div>
${this.configEntryId
? html`<div class="card-actions">
<ha-button
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
appearance="plain"
size="small"
>
${this.hass.localize(
"ui.panel.config.devices.caption"
)}</ha-button
>
<ha-button
appearance="plain"
size="small"
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
>
${this.hass.localize(
"ui.panel.config.entities.caption"
)}</ha-button
>
</div>`
: ""}
<div class="card-actions">
<ha-button
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
appearance="plain"
size="small"
>
${this.hass.localize(
"ui.panel.config.devices.caption"
)}</ha-button
>
<ha-button
appearance="plain"
size="small"
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
>
${this.hass.localize(
"ui.panel.config.entities.caption"
)}</ha-button
>
</div>
</ha-card>
<ha-card
class="network-settings"
@@ -321,6 +319,15 @@ class ZHAConfigDashboard extends LitElement {
`;
}
private async _fetchConfigEntry(): Promise<void> {
const configEntries = await getConfigEntries(this.hass, {
domain: "zha",
});
if (configEntries.length) {
this._configEntry = configEntries[0];
}
}
private async _fetchConfiguration(): Promise<void> {
this._configuration = await fetchZHAConfiguration(this.hass!);
}
@@ -399,20 +406,11 @@ class ZHAConfigDashboard extends LitElement {
fileDownload(backupJSON, `${basename}.json`);
}
private async _openOptionFlow() {
if (!this.configEntryId) {
private _openOptionFlow() {
if (!this._configEntry) {
return;
}
const configEntries: ConfigEntry[] = await getConfigEntries(this.hass, {
domain: "zha",
});
const configEntry = configEntries.find(
(entry) => entry.entry_id === this.configEntryId
);
showOptionsFlowDialog(this, configEntry!);
showOptionsFlowDialog(this, this._configEntry);
}
private _dataChanged(ev) {

View File

@@ -302,10 +302,11 @@ class DialogZWaveJSUpdateFirmwareNode extends LitElement {
</div>
${this._updateFinishedMessage!.success
? html`<p>
${this.hass.localize(
`ui.panel.config.zwave_js.update_firmware.finished_status.done${localizationKeySuffix}`
)}
</p>`
${this.hass.localize(
`ui.panel.config.zwave_js.update_firmware.finished_status.done${localizationKeySuffix}`
)}
</p>
${closeButton}`
: html`<p>
${this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.finished_status.try_again"

View File

@@ -0,0 +1,136 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-list";
import "../../../../../components/ha-list-item";
import "../../../../../layouts/hass-loading-screen";
import "../../../../../layouts/hass-subpage";
import type { ConfigEntry } from "../../../../../data/config_entries";
import { getConfigEntries } from "../../../../../data/config_entries";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { navigate } from "../../../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../../../common/string/compare";
@customElement("zwave_js-config-entry-picker")
class ZWaveJSConfigEntryPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@state() private _configEntries?: ConfigEntry[];
protected async firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
await this._fetchConfigEntries();
}
protected render() {
if (!this._configEntries) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
if (this._configEntries.length === 0) {
return html`
<hass-subpage header="Z-Wave" .narrow=${this.narrow} .hass=${this.hass}>
<div class="content">
<ha-card>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.picker.no_entries"
)}
</p>
</div>
</ha-card>
</div>
</hass-subpage>
`;
}
return html`
<hass-subpage header="Z-Wave" .narrow=${this.narrow} .hass=${this.hass}>
<div class="content">
<ha-card
.header=${this.hass.localize(
"ui.panel.config.zwave_js.picker.title"
)}
>
<ha-list>
${this._configEntries.map(
(entry) => html`
<a
href="/config/zwave_js/dashboard?config_entry=${entry.entry_id}"
>
<ha-list-item hasMeta>
<span>${entry.title}</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
`
)}
</ha-list>
</ha-card>
</div>
</hass-subpage>
`;
}
private async _fetchConfigEntries() {
const entries = await getConfigEntries(this.hass, {
domain: "zwave_js",
});
this._configEntries = entries.sort((a, b) =>
caseInsensitiveStringCompare(a.title, b.title)
);
if (this._configEntries.length === 1) {
navigate(
`/config/zwave_js/dashboard?config_entry=${this._configEntries[0].entry_id}`,
{ replace: true }
);
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.content {
padding: 24px;
display: flex;
justify-content: center;
}
ha-card {
max-width: 600px;
width: 100%;
}
.card-header {
font-size: 20px;
font-weight: 500;
padding: 16px;
padding-bottom: 0;
}
a {
text-decoration: none;
color: inherit;
}
ha-list {
--md-list-item-leading-space: var(--ha-space-4);
--md-list-item-trailing-space: var(--ha-space-4);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zwave_js-config-entry-picker": ZWaveJSConfigEntryPicker;
}
}

View File

@@ -3,9 +3,7 @@ import { customElement, property } from "lit/decorators";
import type { RouterOptions } from "../../../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../../../layouts/hass-router-page";
import type { HomeAssistant } from "../../../../../types";
import { navigate } from "../../../../../common/navigate";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import { getConfigEntries } from "../../../../../data/config_entries";
export const configTabs: PageNavigation[] = [
{
@@ -33,14 +31,36 @@ class ZWaveJSConfigRouter extends HassRouterPage {
@property({ type: Boolean }) public narrow = false;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
private _configEntry: string | null = null;
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
defaultPage: "picker",
showLoading: true,
// Make sure that we have a config entry in the URL before rendering other pages
beforeRender: (page) => {
const searchParams = new URLSearchParams(window.location.search);
if (searchParams.has("config_entry")) {
this._configEntry = searchParams.get("config_entry");
} else if (page === "picker") {
this._configEntry = null;
return undefined;
}
if ((!page || page === "picker") && this._configEntry) {
return "dashboard";
}
if ((!page || page !== "picker") && !this._configEntry) {
return "picker";
}
return undefined;
},
routes: {
picker: {
tag: "zwave_js-config-entry-picker",
load: () => import("./zwave_js-config-entry-picker"),
},
dashboard: {
tag: "zwave_js-config-dashboard",
load: () => import("./zwave_js-config-dashboard"),
@@ -70,7 +90,6 @@ class ZWaveJSConfigRouter extends HassRouterPage {
load: () => import("./zwave_js-network-visualization"),
},
},
initialLoad: () => this._fetchConfigEntries(),
};
protected updatePageEl(el): void {
@@ -79,29 +98,6 @@ class ZWaveJSConfigRouter extends HassRouterPage {
el.isWide = this.isWide;
el.narrow = this.narrow;
el.configEntryId = this._configEntry;
const searchParams = new URLSearchParams(window.location.search);
if (this._configEntry && !searchParams.has("config_entry")) {
searchParams.append("config_entry", this._configEntry);
navigate(
`${this.routeTail.prefix}${
this.routeTail.path
}?${searchParams.toString()}`,
{ replace: true }
);
}
}
private async _fetchConfigEntries() {
if (this._configEntry) {
return;
}
const entries = await getConfigEntries(this.hass, {
domain: "zwave_js",
});
if (entries.length) {
this._configEntry = entries[0].entry_id;
}
}
}

View File

@@ -25,6 +25,8 @@ class HaPanelDevEvent extends LitElement {
@state() private _isValid = true;
@state() private _selectedEventType = "";
protected render(): TemplateResult {
return html`
<div
@@ -89,7 +91,10 @@ class HaPanelDevEvent extends LitElement {
</div>
</ha-card>
<event-subscribe-card .hass=${this.hass}></event-subscribe-card>
<event-subscribe-card
.hass=${this.hass}
.selectedEventType=${this._selectedEventType}
></event-subscribe-card>
</div>
<div>
@@ -109,6 +114,7 @@ class HaPanelDevEvent extends LitElement {
private _eventSelected(ev) {
this._eventType = ev.detail.eventType;
this._selectedEventType = ev.detail.eventType;
}
private _eventTypeChanged(ev) {

View File

@@ -15,6 +15,8 @@ import type { HomeAssistant } from "../../../types";
class EventSubscribeCard extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public selectedEventType = "";
@state() private _eventType = "";
@state() private _subscribed?: () => void;
@@ -36,6 +38,18 @@ class EventSubscribeCard extends LitElement {
}
}
protected willUpdate(changedProperties: Map<string, any>) {
super.willUpdate(changedProperties);
if (
changedProperties.has("selectedEventType") &&
this.selectedEventType &&
!this._subscribed
) {
this._eventType = this.selectedEventType;
}
}
protected render(): TemplateResult {
return html`
<ha-card

View File

@@ -30,35 +30,33 @@ import {
import { formatTime } from "../../../../../common/datetime/format_time";
import type { ECOption } from "../../../../../resources/echarts/echarts";
import { filterXSS } from "../../../../../common/util/xss";
import type { StatisticPeriod } from "../../../../../data/recorder";
export function getSuggestedMax(
dayDifference: number,
end: Date,
detailedDailyData = false
): number {
export function getSuggestedMax(period: StatisticPeriod, end: Date): number {
let suggestedMax = new Date(end);
if (period === "5minute") {
return suggestedMax.getTime();
}
suggestedMax.setMinutes(0, 0, 0);
if (period === "hour") {
return suggestedMax.getTime();
}
// Sometimes around DST we get a time of 0:59 instead of 23:59 as expected.
// Correct for this when showing days/months so we don't get an extra day.
if (dayDifference > 2 && suggestedMax.getHours() === 0) {
if (suggestedMax.getHours() === 0) {
suggestedMax = subHours(suggestedMax, 1);
}
if (!detailedDailyData) {
suggestedMax.setMinutes(0, 0, 0);
}
if (dayDifference > 35) {
suggestedMax.setDate(1);
}
if (dayDifference > 2) {
suggestedMax.setHours(0);
suggestedMax.setHours(0);
if (period === "day" || period === "week") {
return suggestedMax.getTime();
}
// period === month
suggestedMax.setDate(1);
return suggestedMax.getTime();
}
export function getSuggestedPeriod(
dayDifference: number
): "month" | "day" | "hour" {
export function getSuggestedPeriod(dayDifference: number): StatisticPeriod {
return dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour";
}
@@ -96,7 +94,10 @@ export function getCommonOptions(
xAxis: {
type: "time",
min: start,
max: getSuggestedMax(dayDifference, end, detailedDailyData),
max: getSuggestedMax(
detailedDailyData ? "5minute" : getSuggestedPeriod(dayDifference),
end
),
},
yAxis: {
type: "value",

View File

@@ -186,7 +186,7 @@ export class HuiEnergyDevicesGraphCard
params.value[0] as number,
this.hass.locale,
params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined
)} kWh`;
)} kWh ${params.percent ? `(${params.percent} %)` : ""}`;
return `${title}${params.marker} ${params.seriesName}: <div style="direction:ltr; display: inline;">${value}</div>`;
}

View File

@@ -196,6 +196,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (changedProps.has("hass") || changedProps.has("_config")) {
this._computeNames();
}
if (!this._config || !changedProps.has("_config")) {
return;
}
@@ -225,10 +229,6 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
}
}
if (changedProps.has("hass")) {
this._computeNames();
}
if (
changedProps.has("_config") &&
oldConfig?.entities !== this._config.entities
@@ -334,10 +334,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
.maxYAxis=${this._config.max_y_axis}
.startTime=${this._energyStart}
.endTime=${this._energyEnd && this._energyStart
? getSuggestedMax(
differenceInDays(this._energyEnd, this._energyStart),
this._energyEnd
)
? getSuggestedMax(this._period!, this._energyEnd)
: undefined}
.fitYData=${this._config.fit_y_data || false}
.hideLegend=${this._config.hide_legend || false}

View File

@@ -28,11 +28,16 @@ import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import { DEFAULT_HOURS_TO_SHOW, DEFAULT_ZOOM } from "../../cards/hui-map-card";
import type { MapCardConfig, MapEntityConfig } from "../../cards/types";
import "../../components/hui-entity-editor";
import type { EntityConfig } from "../../entity-rows/types";
import "../hui-sub-element-editor";
import type {
EditDetailElementEvent,
SubElementEditorConfig,
EntitiesEditorEvent,
} from "../types";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import type { LovelaceCardEditor } from "../../types";
import { processEditorEntities } from "../process-editor-entities";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import type { EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
export const mapEntitiesConfigStruct = union([
@@ -76,13 +81,20 @@ const cardConfigStruct = assign(
const themeModes = ["auto", "light", "dark"] as const;
const SUB_SCHEMA = [
{ name: "entity", selector: { entity: {} }, required: true },
{ name: "name", selector: { text: {} } },
] as const;
@customElement("hui-map-card-editor")
export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: MapCardConfig;
@state() private _configEntities?: EntityConfig[];
@state() private _subElementEditorConfig?: SubElementEditorConfig;
@state() private _configEntities?: MapEntityConfig[];
@state() private _possibleGeoSources?: { value: string; label?: string }[];
@@ -150,7 +162,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
this._config = config;
this._configEntities = config.entities
? processEditorEntities(config.entities)
? (processEditorEntities(config.entities) as MapEntityConfig[])
: [];
}
@@ -167,6 +179,19 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
return nothing;
}
if (this._subElementEditorConfig) {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.schema=${SUB_SCHEMA}
@go-back=${this._goBack}
@config-changed=${this._handleSubEntityChanged}
>
</hui-sub-element-editor>
`;
}
return html`
<ha-form
.hass=${this.hass}
@@ -180,7 +205,9 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
.hass=${this.hass}
.entities=${this._configEntities}
.entityFilter=${hasLocation}
can-edit
@entities-changed=${this._entitiesValueChanged}
@edit-detail-element=${this._editDetailElement}
></hui-entity-editor>
<h3>
@@ -203,6 +230,36 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
`;
}
private _goBack(): void {
this._subElementEditorConfig = undefined;
}
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
this._subElementEditorConfig = ev.detail.subElementConfig;
}
private _handleSubEntityChanged(ev: CustomEvent): void {
ev.stopPropagation();
const index = this._subElementEditorConfig!.index!;
const newEntities = this._configEntities!.concat();
const newConfig = ev.detail.config as MapEntityConfig;
this._subElementEditorConfig = {
...this._subElementEditorConfig!,
elementConfig: newConfig,
};
newEntities[index] = newConfig;
let config = this._config!;
config = { ...config, entities: newEntities };
this._config = config;
this._configEntities = processEditorEntities(
config.entities as any[]
) as MapEntityConfig[];
fireEvent(this, "config-changed", { config });
}
private _selectSchema = memoizeOne(
(options, localize: LocalizeFunc): SelectSelector => ({
select: {
@@ -229,7 +286,9 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
if (ev.detail && ev.detail.entities) {
this._config = { ...this._config!, entities: ev.detail.entities };
this._configEntities = processEditorEntities(this._config.entities || []);
this._configEntities = processEditorEntities(
this._config.entities || []
) as MapEntityConfig[];
fireEvent(this, "config-changed", { config: this._config! });
}
}

View File

@@ -263,7 +263,8 @@ class HUIRoot extends LitElement {
{
icon: mdiPlus,
key: "ui.panel.lovelace.menu.add",
visible: !this._editMode && this.hass.user?.is_admin,
visible:
!this._editMode && this.hass.user?.is_admin && !this.hass.kioskMode,
overflow: this.narrow,
subItems: [
{
@@ -301,7 +302,7 @@ class HUIRoot extends LitElement {
key: "ui.panel.lovelace.menu.search_entities",
buttonAction: this._showQuickBar,
overflowAction: this._handleShowQuickBar,
visible: !this._editMode,
visible: !this._editMode && !this.hass.kioskMode,
overflow: this.narrow,
suffix:
this.hass.enableShortcuts && !isMobileClient ? "(E)" : undefined,
@@ -349,7 +350,8 @@ class HUIRoot extends LitElement {
visible:
!this._editMode &&
this.hass!.user?.is_admin &&
!this.hass!.config.recovery_mode,
!this.hass!.config.recovery_mode &&
!this.hass.kioskMode,
overflow: true,
overflow_can_promote: true,
},

View File

@@ -77,6 +77,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
resources: null as any,
localize: () => "",
translationMetadata,
kioskMode: false,
dockedSidebar: "docked",
vibrate: true,
debugConnection: __DEV__,

View File

@@ -11,10 +11,12 @@ declare global {
// for fire event
interface HASSDomEvents {
"hass-dock-sidebar": DockSidebarParams;
"hass-kiosk-mode": { enable: boolean };
}
// for add event listener
interface HTMLElementEventMap {
"hass-dock-sidebar": HASSDomEvent<DockSidebarParams>;
"hass-kiosk-mode": HASSDomEvent<HASSDomEvents["hass-kiosk-mode"]>;
}
}
@@ -26,5 +28,8 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
this._updateHass({ dockedSidebar: ev.detail.dock });
storeState(this.hass!);
});
window.addEventListener("hass-kiosk-mode", (ev) => {
this._updateHass({ kioskMode: ev.detail.enable });
});
}
};

View File

@@ -137,9 +137,9 @@
},
"counter": {
"actions": {
"increment": "increment",
"decrement": "decrement",
"reset": "reset"
"increment": "Increment",
"decrement": "Decrement",
"reset": "Reset"
}
},
"cover": {
@@ -672,8 +672,8 @@
"device_missing": "No related device"
},
"add": "Add",
"custom_name": "Custom name",
"no_match": "No entities found"
"search": "Search or enter custom name",
"custom_name": "Custom name"
},
"entity-attribute-picker": {
"attribute": "Attribute",
@@ -685,7 +685,7 @@
},
"entity-state-content-picker": {
"add": "Add",
"custom_state": "Custom state"
"custom_attribute": "Custom attribute"
}
},
"target-picker": {
@@ -772,9 +772,6 @@
"no_match": "No languages found for {term}",
"no_languages": "No languages available"
},
"icon-picker": {
"no_match": "No matching icons found"
},
"tts-picker": {
"tts": "Text-to-speech",
"none": "None"
@@ -2333,6 +2330,27 @@
},
"cloud": {
"secondary": "Loading..."
},
"zwave_js": {
"secondary": "Sub-GHz mesh protocol"
},
"zha": {
"secondary": "Low-power mesh network"
},
"matter": {
"secondary": "Cross-vendor smart home standard"
},
"thread": {
"secondary": "Mesh network often used for Matter devices"
},
"bluetooth": {
"secondary": "Local device connectivity"
},
"knx": {
"secondary": "Building automation standard"
},
"insteon": {
"secondary": "Dual-mesh home automation"
}
},
"common": {
@@ -4069,7 +4087,7 @@
"new_automation_setup_failed_title": "New {type} setup timed out",
"new_automation_setup_failed_text": "Your new {type} was saved, but waiting for it to set up has timed out. This could be due to errors parsing your configuration.yaml, please check the configuration in developer tools. Your {type} will not be visible until this is corrected and {types} are reloaded. Changes to area, category, or labels were not saved and must be reapplied.",
"new_automation_setup_keep_waiting": "You may continue to wait for a response from the server, in case it is just taking an unusually long time to process this {type}.",
"new_automation_setup_timedout_success": "The server has responded and this has now setup successfully. You may now close this dialog.",
"new_automation_setup_timedout_success": "The server has responded and this has now set up successfully. You may now close this dialog.",
"item_pasted": "{item} pasted",
"ctrl": "Ctrl",
"del": "Del",
@@ -4098,7 +4116,8 @@
"targets": "{count} {count, plural,\n one {target}\n other {targets}\n}",
"invalid": "Invalid target",
"all_entities": "All entities",
"none_entities": "No entities"
"none_entities": "No entities",
"template": "Template"
},
"triggers": {
"name": "Triggers",
@@ -5992,26 +6011,28 @@
},
"bluetooth": {
"title": "Bluetooth",
"settings_title": "Bluetooth settings",
"tabs": {
"overview": "Overview",
"advertisements": "Advertisements",
"visualization": "Visualization",
"connections": "Connections"
},
"settings_title": "Bluetooth adapters",
"option_flow": "Configure Bluetooth options",
"advertisement_monitor": "Advertisement monitor",
"advertisement_monitor_details": "The advertisement monitor listens for Bluetooth advertisements and displays the data in a structured format.",
"connection_slot_allocations_monitor": "Connection slot allocations monitor",
"connection_slot_allocations_monitor_details": "The connection slot allocations monitor displays the (GATT) connection slot allocations for the adapter. This adapter supports up to {slots} simultaneous connections. Each remote Bluetooth device that requires an active connection will use one connection slot while the Bluetooth device is connecting or connected.",
"connection_monitor": "Connection monitor",
"visualization": "Visualization",
"used_connection_slot_allocations": "Used connection slot allocations",
"no_connections": "No active connections",
"active_connections": "connections",
"no_advertisements_found": "No matching Bluetooth advertisements found",
"no_connection_slot_allocations": "No connection slot allocations information available",
"no_active_connection_support": "This adapter does not support making active (GATT) connections.",
"no_connection_slots": "No connection slots",
"no_scanner_state_available": "No scanner state available",
"current_scanning_mode": "Current scanning mode",
"requested_scanning_mode": "Requested scanning mode",
"scanner_state_unknown": "State unknown",
"scanning_mode_none": "none",
"scanning_mode_active": "active",
"scanning_mode_passive": "passive",
"scanner_mode_mismatch": "Scanner requested {requested} mode but is operating in {current} mode. The scanner is in a bad state and needs to be power cycled.",
"scanning_mode_active_label": "Active scanning",
"scanning_mode_passive_label": "Passive scanning",
"scanning_mode_none_label": "No scanning",
"scanner_mode_mismatch": "{name} requested {requested} mode but is operating in {current} mode. The scanner is in a bad state and needs to be power cycled.",
"scanner_mode_mismatch_remote": "For proxies: reboot the device",
"scanner_mode_mismatch_usb": "For USB adapters: unplug and plug back in",
"scanner_mode_mismatch_uart": "For UART/onboard adapters: power down the system completely and power it back up",
@@ -6029,7 +6050,6 @@
"service_uuids": "Service UUIDs",
"copy_to_clipboard": "[%key:ui::panel::config::automation::editor::copy_to_clipboard%]",
"area": "Area",
"core": "Home Assistant",
"scanners": "Scanners",
"known_devices": "Known devices",
"unknown_devices": "Unknown devices"
@@ -6232,7 +6252,7 @@
"new_channel": "New channel",
"change_channel": "Change channel",
"migration_warning": "Zigbee channel migration is an experimental feature and relies on devices on your network to support it. Device support for this feature varies and only a portion of your network may end up migrating! It may take up to an hour for changes to propagate to all devices.",
"description": "Change your Zigbee channel only after you have eliminated all other sources of 2.4GHz interference by using a USB extension cable and moving your coordinator away from USB 3.0 devices and ports, SSDs, 2.4GHz WiFi networks on the same channel, motherboards, and so on.",
"description": "Change your Zigbee channel only after you have eliminated all other sources of 2.4GHz interference by using a USB extension cable and moving your coordinator away from USB 3.0 devices and ports, SSDs, 2.4GHz Wi-Fi networks on the same channel, motherboards, and so on.",
"smart_explanation": "It is recommended to use the \"Smart\" option once your environment is optimized as opposed to manually choosing a channel, as it picks the best channel for you after scanning all Zigbee channels. This does not configure ZHA to automatically change channels in the future, it only changes the channel a single time.",
"channel_has_been_changed": "Network channel has been changed",
"devices_will_rejoin": "Devices will re-join the network over time. This may take a few minutes.",
@@ -6806,9 +6826,50 @@
}
}
}
},
"picker": {
"title": "Select Z-Wave network",
"no_entries": "No Z-Wave networks configured. Set up the Z-Wave JS integration first."
}
},
"matter": {
"panel": {
"thread_panel": "Visit Thread Panel",
"experimental_note": "Matter is still in the early phase of development, it is not meant to be used in production. This panel is for development only.",
"add_devices": "You can add Matter devices by commissioning them if they are not set up yet, or share them from another controller and enter the sharing code.",
"mobile_app_commisioning": "Commission device with mobile app",
"commission_device": "Commission device",
"add_shared_device": "Add shared device",
"set_wifi_credentials": "Set Wi-Fi credentials",
"set_thread_credentials": "Set Thread credentials",
"prompts": {
"network_name": {
"title": "Network name",
"input_label": "Network name",
"confirm": "Continue"
},
"passcode": {
"title": "Passcode",
"input_label": "Code",
"confirm": "Set Wi-Fi"
},
"commission_device": {
"title": "Commission device",
"input_label": "Code",
"confirm": "Commission"
},
"add_shared_device": {
"title": "Add shared device",
"input_label": "PIN",
"confirm": "Accept device"
},
"set_thread": {
"title": "Set Thread operation",
"input_label": "Dataset",
"confirm": "Set Thread"
}
}
},
"network_type": {
"thread": "Thread",
"wifi": "Wi-Fi",
@@ -7283,7 +7344,7 @@
"energy_usage_graph": {
"total_consumed": "Total consumed {num} kWh",
"total_returned": "Total returned {num} kWh",
"total_usage": "{num} kWh used",
"total_usage": "+{num} kWh",
"combined_from_grid": "Combined from grid",
"consumed_solar": "Consumed solar",
"consumed_battery": "Consumed battery"

View File

@@ -250,6 +250,7 @@ export interface HomeAssistant {
enableShortcuts: boolean;
vibrate: boolean;
debugConnection: boolean;
kioskMode: boolean;
dockedSidebar: "docked" | "always_hidden" | "auto";
moreInfoEntityId: string | null;
user?: CurrentUser;

721
yarn.lock

File diff suppressed because it is too large Load Diff