Compare commits

..

432 Commits

Author SHA1 Message Date
ildar170975
73a1ce90c3 Data tables: do not show "Assistants" column in "secondary" when narrow (#29120)
fix a column for "narrow"
2026-01-22 09:37:03 +02:00
uptimeZERO_
ea1b7b9dec Media player fixes (#29075)
* aligning ui of dialog and media bar

* refactored media progress logic to be reusable

* updating track times to be consistent with music assistant

* WIP aligning volume slider with music assistant

* migrating to ha-dropdown

* showing volume tooltip on touch devices

* Fixed volume slider going to 100 randomly

* Added scrolling support

* Refactored volume control logic
2026-01-21 20:28:32 +01:00
Paul Bottein
fd506d4d72 Add assign area shortcut to home panel (#29082)
* Add assign area shortcut to home panel

* Set empty state for empty other devices page
2026-01-21 17:11:23 +01:00
karwosts
a3be09018c Fix add entry button for integrations (#29106) 2026-01-21 16:13:03 +02:00
Wendelin
3364d4f578 Migrate button-menu components to dropdown in 7 files (#29105)
Migrate button-menu components to dropdown in various editors
2026-01-21 15:12:15 +02:00
Paul Bottein
1f04379974 Add button to heading card (#28991)
* Add heading badge button

* Fix look and feel

* Improve editor

* Prettier
2026-01-21 11:38:48 +01:00
Kristel
e060c179f6 Refactor ha-automation-picker _applyFilters (#29055) 2026-01-21 10:56:01 +01:00
Wendelin
54b72ce2b8 Migrate button-menu to ha-dropdown 8 files (#29102) 2026-01-21 09:35:25 +00:00
Aidan Timson
5795b8787d Allow helpers area id to fallback to device area if not set (#29093) 2026-01-21 10:22:38 +01:00
Paulus Schoutsen
ec742d3342 App store link app panel 2 (#29100)
* Revert "App store to link to app panel"

This reverts commit 415319f69e.

* App store to link to app panel
2026-01-21 06:04:45 +01:00
renovate[bot]
e4d6f3c9d7 Update dependency tar to v7.5.3 [SECURITY] (#29045)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 17:12:23 +00:00
Paul Bottein
2d496afdbc Add discovered devices to add integration dialog (#29092) 2026-01-20 14:28:29 +00:00
Wendelin
681b60614f Migrate button-menu to ha-dropdown in 9 files (#29089)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-20 14:20:29 +00:00
Petar Petrov
1654a67d30 Refactor area control picker into a separate component (#29080) 2026-01-20 15:04:20 +01:00
renovate[bot]
8f00494d53 Update dependency rspack-manifest-plugin to v5.2.1 (#29091)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 15:31:53 +02:00
Joost Lekkerkerker
d9c7c0422b Allow the main entry type button to be translatable (#28721)
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2026-01-20 14:25:48 +01:00
Aidan Timson
2d24447c3c Rename Add-ons to Apps in more areas (#29076)
* Remove add-ons mention in agents file

* Rename user facing form and selector name

* Everything else

* Update more

* Update more

* Update more

* Update more

* Update key

* Update key

* Update keys

* Use translation

* More changes

* Update key

* Backward / Forward compat

* Drop "the"

Co-authored-by: Norbert Rittel <norbert@rittel.de>

---------

Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-01-20 13:39:30 +02:00
Paul Bottein
3b8d485ec6 Update log icons (#29084) 2026-01-20 11:40:03 +01:00
Wendelin
4e4a00e3e9 Migrate ha-button-menu to ha-dropdown in 6 files (#29072)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-20 09:29:29 +00:00
Paul Bottein
14f7328f92 Add context to group more info (#29077)
* Add area context in more info group

* Use entity name instead of entry

* Remove filter
2026-01-20 10:26:03 +02:00
Paul Bottein
c5ad074dfb Create reusable ha tile container component. (#29038)
* Create reusable component for tile based card

* Fix icon interaction

* Add icon and iconPath props

* Migrate discovered devices card

* Refactor

* Share card style
2026-01-20 08:49:10 +02:00
Paulus Schoutsen
07aa8706ce App store to link to app panel (#29079) 2026-01-20 06:11:56 +01:00
TheJulianJES
1665fa3775 Fix Z-Wave dashboard picker showing disabled config entries (#29078)
* Fix Z-Wave dashboard picker showing disabled config entries

* Fix Z-Wave dashboard picker showing ignored discoveries
2026-01-19 20:08:44 +01:00
Aidan Timson
3be6a87658 Bring scene editor in line with automations and scripts (#29002)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-01-19 16:49:54 +01:00
ildar170975
9092de5c28 Entity card: add support of actions (#28949) 2026-01-19 15:13:36 +01:00
Petar Petrov
e0fc661920 Allow specific entity controls in Area card (#29025)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-01-19 14:44:10 +01:00
JLo
aaad8e5434 Add distribution card (#28886)
* Add horizontal stacked bar card

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* Refactor to use ha-segmented-bar component

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* Fix spacing when heading is empty

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* Rename card from horizontal-stacked-bar to distribution

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* Fix remaining translation references

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* Add fixed row setting to prevent layout issues

* Add spacing between bar and legend

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* PR review changes

* Improve accessibility and performance

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-19 12:46:14 +00:00
Jan Layola
35c668744a Update ha-base-time-input to accept decimal input for seconds (#29058)
* Update ha-base-time-input to accept decimal input for seconds

* Add support for decimal values in time formatting ha-base-time-input
2026-01-19 14:28:08 +02:00
Wendelin
081b0a0222 Migrate ha-button-menu to ha-dropdown in 8 files (#29070)
Migrates the following files from ha-button-menu to ha-dropdown component:
- ha-config-backup-details.ts
- ha-config-areas-dashboard.ts
- ha-config-dashboard.ts
- ha-panel-history.ts
- ha-scene-editor.ts
- ha-config-integrations-dashboard.ts
- thread-config-panel.ts
- ha-panel-developer-tools.ts

Changes:
- Replace ha-button-menu with ha-dropdown
- Replace ha-list-item with ha-dropdown-item
- Change @action event handlers to @wa-select
- Change slot="graphic" to slot="icon" for icons
- Update event handlers from index-based to value-based selection
- Replace <li divider role="separator"> with <wa-divider>
- Update CSS selectors from ha-button-menu to ha-dropdown
- Use type="checkbox" for checkbox dropdown items in integrations dashboard
- Remove unused li[role="separator"] CSS styles

Contributes to #26537

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 13:11:29 +02:00
Paulus Schoutsen
829cd96e9b Add apps panel as built-in panel (#28245)
* Add apps panel as built-in panel

* Fix missing translation

* Address cursor comment

* Another cursor fix

* One more cursor fix

* Address PR review comments: localize error messages, fix deprecated method, use spacing tokens (#28246)

* Initial plan

* Address PR review comments: localize error messages, replace substr with substring, use spacing tokens

Co-authored-by: balloob <1444314+balloob@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: balloob <1444314+balloob@users.noreply.github.com>

* Cursor fix

* Cursor: use willUpdate

* prettier

* Cursor: fix translation placeholder swap

* Apply suggestions from code review

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

* Cursor: clearTimeout

* Cursor: fix race condition

* Update src/translations/en.json

Co-authored-by: Norbert Rittel <norbert@rittel.de>

* Apply spacing tokens

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: balloob <1444314+balloob@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-01-19 13:08:15 +02:00
Paulus Schoutsen
c404e66ee5 Add new app panel (#28214)
* Add ingress panel to Home Assistant

* Better wait until app loaded logic

* Cleaner slug extraction

* Support HA-aware apps integrating more tightly

* Add new file too

* Memoize

* Cursor: use clearTimeout

* Cursor: fix race conditions

* Claude: fixes

* Cursor: fix issues

* Rename hideToolbar to kioskMode

* Hook kiosk mode into native kiosk mode

* Update src/data/route.ts

* Move computeRouteTail to common URL module

* dry refactor

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-19 13:08:07 +02:00
Aidan Timson
d47d3f9694 Create shared ai task metadata suggestion task (#29012)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-01-19 09:42:36 +01:00
dependabot[bot]
622df52167 Bump actions/cache from 5.0.1 to 5.0.2 (#29068)
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.1 to 5.0.2.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](9255dc7a25...8b402f58fb)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.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-19 09:44:26 +02:00
dependabot[bot]
20345c3771 Bump actions/setup-node from 6.1.0 to 6.2.0 (#29067)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](395ad32622...6044e13b5d)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-19 09:44:01 +02:00
dependabot[bot]
fc7468a43b Bump github/codeql-action from 4.31.9 to 4.31.10 (#29069)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.9 to 4.31.10.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](5d4e8d1aca...cdefb33c0f)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.10
  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-19 09:43:43 +02:00
renovate[bot]
c8ab65cde9 Lock file maintenance (#29065)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 08:01:36 +02:00
Kristel
001ade24ea Reuse <voice-assistant-brand-icon> (#29046) 2026-01-18 23:55:53 +01:00
karwosts
f987cfe91e Use water-heater operation_mode icon translations (#29051) 2026-01-18 23:31:11 +01:00
renovate[bot]
0bc0acebe0 Update formatjs monorepo (#29062)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-18 23:30:07 +01:00
renovate[bot]
2087efca51 Update dependency prettier to v3.8.0 (#29049)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-18 09:57:51 +01:00
renovate[bot]
16a4a07080 Update dependency @rsdoctor/rspack-plugin to v1.5.0 (#29044)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 13:26:05 +01:00
renovate[bot]
58eefcb216 Update CodeMirror (#29043)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 13:24:01 +01:00
Wendelin
92a36ac687 Migrate ha-button-menu to ha-dropdown in logs and forms (#29036)
Migrate four components from ha-button-menu to ha-dropdown:
- error-log-card: overflow menu with log view switching
- system-log-card: overflow menu for full logs
- ha-filter-categories: category edit/delete actions
- ha-form-optional_actions: add interaction dropdown

Changes:
- Replace ha-button-menu with ha-dropdown
- Replace ha-list-item with ha-dropdown-item
- Update event handlers from @action to @wa-select
- Use value-based selection instead of index-based
- Add proper accessibility labels to trigger buttons

Part of #26537

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 19:27:14 +01:00
Wendelin
8221ca8971 Add artifact upload step for frontend build in CI workflow (#29034) 2026-01-16 17:30:37 +01:00
Paul Bottein
8f69cbb6c1 Add discovered devices card (#29035) 2026-01-16 17:25:08 +01:00
renovate[bot]
4c111e1a7d Update dependency @codemirror/search to v6.6.0 (#29029)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 15:17:31 +02:00
Aidan Timson
af8659d8ed Migrate automation/script dialogs to ha-wa-dialog (#29030)
* Migrate dialog-automation-save to ha-wa-dialog

* Migrate dialog-automation-mode to ha-wa-dialog

* Migrate dialog-paste-replace to ha-wa-dialog

* Migrate dialog-new-automation to ha-wa-dialog

* Migrate ha-device-automation-dialog to ha-wa-dialog
2026-01-16 13:23:56 +02:00
renovate[bot]
c9e1c9e0a3 Update dependency ua-parser-js to v2.0.8 (#29028)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 10:14:57 +00:00
renovate[bot]
9d15499953 Update dependency @rspack/core to v1.7.2 (#29027)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 10:03:25 +00:00
Wendelin
7d54dd4940 Fix shortcuts ctrl translation (#29024)
* fix: update localization for CTRL key in shortcuts dialog

* fix: update search label in translations for improved clarity
2026-01-16 11:09:34 +02:00
renovate[bot]
8ef717df6e Update dependency typescript-eslint to v8.53.0 (#29019)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 11:08:06 +02:00
renovate[bot]
7b4a7403c8 Update babel monorepo to v7.28.6 (#29018)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 11:07:40 +02:00
ildar170975
22b7c52828 "Voice assistant" column in tables: standardize (#28889)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-01-16 09:10:02 +01:00
Remy Sharp
04dbeb5e84 Fixed dark mode hidden text in debug assistant (#29021)
Co-authored-by: Wendelin <w@pe8.at>
2026-01-16 08:33:45 +01:00
Simon Lamon
db5f823b6b Remove twine and introduce trusted publishing (#27110)
* Remove twine and introduce trusted publishing

* Update release.yaml
2026-01-15 22:50:52 -05:00
uptimeZERO_
1d241aa49a Truncate long menu item labels in the sidebar (#29005) 2026-01-15 15:41:48 +00:00
Pegasus
fece231faf fix: restrict to exact match for data table (#28853) 2026-01-15 15:50:15 +01:00
Aidan Timson
fffb3c3a28 Migrate category dialogs to ha-wa-dialog (#29009) 2026-01-15 15:32:43 +01:00
renovate[bot]
fe14d436ff Update vitest monorepo to v4.0.17 (#29007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-15 15:46:10 +02:00
Petar Petrov
42e02be928 Add subpage titles in for config panel pages (#28990)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-01-15 12:48:33 +01:00
Aydar Gumerbaev
6213b6cd2a Always use fallback for brands URL (#28994) 2026-01-15 11:42:17 +00:00
Aidan Timson
cd75c55392 Entity context: voice assistants expose entities (#28992)
* Entity context: voice assistants expose entities

* Load virtualiser

* Refactor filter entities, reduce duplicate renders

* Fix logic
2026-01-15 13:09:38 +02:00
Marcin Bauer
ca325020d7 Add Labs feature note to automation element picker (#28874)
Co-authored-by: Wendelin <w@pe8.at>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-01-15 11:09:11 +00:00
Jeremy Cook
6250402661 Fix vertical-align in markdown tables with presentation role (#29001) 2026-01-15 11:48:43 +01:00
Amit Finkelstein
0bfca79851 Stop dropdown select events from bubbling in automation rows (#28985) 2026-01-15 11:22:23 +01:00
Wendelin
49bddf6139 Automation add TCA: fix: prevent multiple dialog closures by tracking closing state (#28978) 2026-01-15 08:50:55 +01:00
Wendelin
0daf94e98f Quick bar: new design and area search (#28678)
* Add "Commands" title to quick bar translations in English

* Enhance QuickBar dialog handling and localize commands title

* add nav icons

* Add icons and styles and separate navigation from commands

* handle non admin

* Add areas

* Fix import and shortcuts

* Restructure

* remove area sort

* move keys

* area search keys review

* Fix adaptive dialog slots without header

* Design review

* Review marcin

* Fix safe area bottom

* Fix ios focus

* Make it clearable

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2026-01-15 09:45:57 +02:00
uptimeZERO_
00a3237611 Persist theme settings to user profile and allow migration (#28965)
* Moved theme functionality and persistence target

* fixed type mismatch

* using SubscribeMixin

* returning no-op unsub to handle rejection path

* added notification if theme save fails

* using hass instead of state

* renamed theme variable for clarity

* Added toast if theme pref is unavailable

* Always saving theme to localStorage

* Removing localStorage fallback

* Updating local cache when new theme comes from core
2026-01-15 09:40:43 +02:00
StormDev
53deb3f419 Removed uneccessary import in landing-page-network.ts (#29000)
* Removed uneccessary console on landing-page-networks.ts

* Uneccessary import removed in landing-page-networks.ts
2026-01-15 07:26:50 +00:00
Pegasus
6c1c7cead3 Fixes duplicate "Device info" section name when viewing Matter devices. (#28984)
ha-device-info-matter: rename 'Device info' to 'Matter info'

Fixes duplicate "Device info" section name when viewing Matter devices.
The nested expansion panel now displays "Matter info" consistent with
other integrations (e.g., ZHA uses "Zigbee info", Z-Wave uses "Z-Wave info").

Also adds a gallery demo for testing the component.
2026-01-15 09:18:21 +02:00
Jason Madigan
f8d65cc0ec Make entities on the energy now sankey graph clickable (#28998)
* enhancement: make entities on the energy now sankey graph clickable to show details

Signed-off-by: Jason Madigan <jason@jasonmadigan.com>

* add a test

Signed-off-by: Jason Madigan <jason@jasonmadigan.com>

* format

---------

Signed-off-by: Jason Madigan <jason@jasonmadigan.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-15 06:44:03 +00:00
Paul Bottein
5be7bad176 Allow to add context to tile card secondary line (#28995) 2026-01-14 19:48:27 +01:00
Paul Bottein
0a54a93a39 Use tabs for bluetooth panel (#28824)
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-14 14:38:10 +01:00
Petar Petrov
156583aff1 Include the area when duplicating a scene from the scene dashboard (#28955) 2026-01-14 12:28:05 +01:00
Aidan Timson
7572257821 Match expose config dashboard for assistants columns (#28956) 2026-01-14 11:43:56 +01:00
Pegasus
4703cf802f Change border-quiet token values from 80 to 90 (#28976) 2026-01-14 09:28:32 +00:00
ildar170975
55c2315329 ha-label-picker: remove valueRenderer (#28975) 2026-01-14 10:15:39 +01:00
Wendelin
7d7e95ac55 Improve device automation UI (#28967)
* Improve device automation rows

* Improve device automation type picker

* Update src/panels/config/automation/condition/ha-automation-condition-row.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-14 07:06:56 +00:00
ildar170975
6d7694caff ha-label-picker, ha-category-picker: fix icon for "no items available" (#28973)
* remove NO_LABELS

* remove NO_CATEGORIES

* reverted removed icon
2026-01-14 08:42:24 +02:00
calm
d7b6243698 Fix tree view heading overlapping Show more button (#28872) (#28968) 2026-01-13 18:34:39 +01:00
calm
73feef9e92 Remove box-shadow from automation dialog "Show more" button (#28945) (#28960) 2026-01-13 17:31:55 +01:00
renovate[bot]
453a546574 Update Node.js to v24.13.0 (#28963)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-13 15:16:03 +00:00
Petar Petrov
52c0e6f1f5 Respect user-configured grid options for fixed_rows/fixed_columns cards (#28961) 2026-01-13 16:24:25 +02:00
Aidan Timson
444f8d87b3 Ignore all node_modules, not just from root dir (#28959) 2026-01-13 13:51:54 +01:00
Pegasus
57a586c3a7 fix: update the z-index of search button mainly for yaml mode (#28878) 2026-01-13 13:41:53 +01:00
Pegasus
1975265e6b Update the Select Option type from any to string per documentation (#28954) 2026-01-13 10:44:02 +01:00
Wendelin
66e6cb8dbc Fix category-picker unknown check (#28957) 2026-01-13 09:39:05 +00:00
Petar Petrov
9ce9d254f8 Picture elements position by click (#28597) 2026-01-13 10:01:07 +01:00
ildar170975
1beca4bfa6 ha-data-table: issues with "numeric" column (#28916)
Co-authored-by: uptimeZERO_ <pavilionsahota@gmail.com>
2026-01-13 08:38:15 +00:00
Kristel
82ab29cfc5 Add "Voice assistant" filter to helpers, automations, scenes and scripts pages (#28914) 2026-01-13 08:29:28 +00:00
Simon Lamon
3579c66f71 Update dropdown adjustments (#28294) 2026-01-13 08:54:17 +01:00
ildar170975
c042a8e310 ha-sidebar: remove scrollIntoViewIfNeeded() (#28938)
remove scrollIntoViewIfNeeded()
2026-01-13 07:23:20 +01:00
renovate[bot]
8d2794a4ee Update dependency vite-tsconfig-paths to v6.0.4 (#28952)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-13 07:15:15 +01:00
Paul Bottein
50be1d9345 Use action button text name for empty state card (#28948) 2026-01-12 17:42:01 +01:00
Petar Petrov
c551bf03b6 Sanitize names in history card and map card (#28947) 2026-01-12 15:28:32 +00:00
Paul Bottein
cd062293fc Add config to empty state card and use it in area empty page (#28946)
* Add config to empty state card and use it in area empty page

* Remove old translations
2026-01-12 16:58:59 +02:00
TheJulianJES
e89ea47d3a Add Matter status to config dashboard (#28825)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2026-01-12 15:45:18 +01:00
SmartCoder
2cd209a6a4 Fixed modal visibility issue in settings -> areas -> edit room (#28907)
* Fixed modal visibility issue in settings -> areas -> edit room

* converting both components to use ha-wa-dialog

* removed z-index from ha-wa-dialog

* fixed hardcoded .open in media browser dialog and remove unnecessary z-index CSS variables
2026-01-12 15:07:56 +02:00
Marcin Bauer
9bbc761736 Fix: Allow dismissing add integration and helper dialogs with escape/click (#28944)
* refactor: polish automation dialog UI and component styles

* Revert "Merge pull request #1 from marcinbauer85/fix/ui-polish-automation-dialog"

This reverts commit c2c47197e2, reversing
changes made to 49bed5e6a6.

* Fix: Allow dismissing add integration and helper dialogs

* Apply suggestions from code review

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-01-12 13:39:16 +01:00
Daniel O'Connor
9097faa04b Config > Helpers > Add loading filter state from URL (#28924) 2026-01-12 13:38:04 +01:00
SmartCoder
fcf844cf1a Fix issue #28896: "Last 12 months" in the Datetime Picker selects last year (#28902)
Summary of the fix:
The Problem:
now-12m was selecting the calendar year (Jan 1st to Dec 31st) instead of the last 12 months from now
It used startOfMonth and endOfMonth, which snap to month boundaries
The Solution:
Changed to match the now-7d and now-30d pattern
Now uses subMonths(today, 12) for start and subMonths(today, 0) (which equals today) for end
This gives exactly the last 12 months (365/366 days) ending at the current time
The Fix:
// Before (WRONG):calcDate(subMonths(today, 12), startOfMonth, ...)  // Jan 1st of 12 months agocalcDate(subMonths(today, 1), endOfMonth, ...)     // Dec 31st of last month// After (CORRECT):calcDate(today, subMonths, hass.locale, hass.config, 12)  // 12 months ago from nowcalcDate(today, subMonths, hass.locale, hass.config, 0)   // now
2026-01-12 11:53:08 +00:00
dcapslock
8808c31e98 Fix ha-card styling of .card-content when not first element but not following .card-header (#28935) 2026-01-12 12:41:14 +01:00
Michael
e0a9f5a08a Show also not installable updates on update overview page (#28717)
* add "show not installable option" to update page

* split updates by install feature and show always

* fix

* fix "no update" panel

* use `nothing` instead of empty string

* re-add `outlined` to ha-card

* keep title, use different for not-installable updates
2026-01-12 13:18:53 +02:00
Petar Petrov
56d71c8e54 Use temp & humidity data from attributes in Area card (#28530)
* Use temp & humidity data from attributes in Area card

* Avoid duplicate sensor readings by tracking devices contributing values
2026-01-12 12:01:12 +01:00
karwosts
125ab4c671 Update energy summary visibility condition (#28913)
* Update energy summary visibility condition

* add grid power as special case

* Always show summary when you have powersource
2026-01-12 12:42:16 +02:00
Eduardo Tsen
8014216c45 Fix ha-entity-toggle not restoring old state on exception (#28915) 2026-01-12 10:28:23 +00:00
ildar170975
55ba331489 developer-tools-statistics: alignment for "fix" column (#28942) 2026-01-12 11:25:44 +01:00
karwosts
ad2ff672b0 Add configurable confirmation title & button text (#28931) 2026-01-12 10:19:09 +00:00
JLo
00907ecd17 Add area and device context to media player join dialog (#28926)
* Add area and device context to media player join dialog

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* Add memoization to avoid recomputing display data

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-12 11:08:44 +01:00
Petar Petrov
07d8219136 Add ES5-compatible keyed directive implementation (#28941) 2026-01-12 10:50:38 +01:00
Eduardo Tsen
f37241c84c Fix hui-select-entity-row restoring old state (#28918) 2026-01-12 09:43:31 +00:00
SmartCoder
65d046132d Updated entity name to friendly name (#28928) 2026-01-12 10:14:23 +01:00
Simon Lamon
122cf40092 Don't close dialog upon tooltip close (#28927) 2026-01-11 20:23:42 -05:00
SmartCoder
28ed5c86c7 Fix automation row menu icon being pushed off-screen on mobile (#28893)
When entity names are too long, the header text would push the three-dot menu icon off the right edge of the screen, making it inaccessible. This fix ensures the menu icon remains visible by:

- Adding min-width: 0 to the header slot to allow proper flexbox shrinking and text wrapping

- Adding flex-shrink: 0 to the icons container to prevent it from being compressed

The fix uses standard flexbox properties that work universally across all screen sizes, ensuring the menu icon stays visible on both mobile and desktop views.
2026-01-10 15:58:28 +01:00
Kristel
1f99c3d895 Add Voice assistants filter to Entities page (#28854)
* create Assistants filter

* render logo and name

* make the Voice assistants filter work

* integrate cloudStatus

* code clean-up

* remove cloudStatus

* bugfix

* remove console log

* remove cloudstatus

* set ha-list clientHeight to 49px
2026-01-10 12:57:33 +01:00
LG-ThinQ-Integration
f2293713de Add target_humidity_step to humidifier (#28005)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2026-01-10 10:12:55 +01:00
Brendan Annable
b3f202400c Fix timer restore bug (#28898) 2026-01-10 09:51:19 +01:00
ildar170975
010d87bd0d ha-dialog-automation-save: small improvements & fixes (#28561)
* explictly set line-height for "helper" element

* move "description" to bottom, css tweaks

* revert

* revert, make a helper persistent
2026-01-10 09:40:10 +01:00
karwosts
b403b8f09e Implement allow_negative for duration selector (#28909) 2026-01-10 08:58:14 +01:00
karwosts
b9a3dc795b Duration selector: migrate legacy duration formats (#28880) 2026-01-09 20:30:09 +01:00
Bram Kragten
35dbfdebcf Add support for choose selector to initial form data (#28876)
* Add support for choose selector to initial form data

* Update compute-initial-ha-form-data.ts
2026-01-09 19:57:32 +01:00
renovate[bot]
c5e5fb3ace Update formatjs monorepo (#28905)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 17:41:54 +00:00
Yosi Levy
e649472b20 Arrow fixes in media browser (#28890) 2026-01-09 18:31:25 +01:00
Yosi Levy
3cbb24a4c5 Fix for volume scroll in media player (#28891) 2026-01-09 18:30:45 +01:00
renovate[bot]
f92608a9d3 Update dependency @codemirror/view to v6.39.9 (#28903)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 18:23:11 +01:00
renovate[bot]
6591cdc5c1 Update dependency @rspack/core to v1.7.1 (#28892)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 18:22:51 +01:00
renovate[bot]
0ae1ac367d Update dependency lit-html to v3.3.2 (#28762)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 08:32:27 +02:00
renovate[bot]
6d3a1b93e1 Update dependency lit to v3.3.2 (#28761)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-08 21:03:20 +01:00
renovate[bot]
6d7b22a21c Update dependency typescript-eslint to v8.52.0 (#28879)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-08 20:54:45 +01:00
Petar Petrov
784ee22623 Removes duplicate closing tag in ha-auth-form-string (#28883) 2026-01-08 20:53:55 +01:00
Aidan Timson
c03654ef8e Fix wa dialog esc behaviour when preventing scrim closure (#28875)
* Fix wa dialog esc behaviour when preventing scrim closure

* Use wa-hide event to prevend closure
2026-01-08 17:10:53 +00:00
Pegasus
826cb3117d Fix: update the id, pan id to capitalize (#28873)
fix: update the id, pan id to capitalize
2026-01-08 12:26:49 +00:00
Aidan Timson
f77fa26ffe Fix type error for calendar card (#28869) 2026-01-08 12:57:46 +02:00
Bram Kragten
35e30f9184 Fix color palette creation (#28867) 2026-01-08 10:14:03 +00:00
DAccord
7dd3ade678 Handling empty history (#28852)
Co-authored-by: DAccord <11232265+DAccord@users.noreply.github.com>
2026-01-08 09:58:38 +00:00
karwosts
6d1e15d11a Add a devtools event listener filter (#28849) 2026-01-08 10:58:24 +01:00
Timothy
f5b33922ff Move companion app settings to a dedicated section in the settings (#28830) 2026-01-08 10:36:50 +01:00
dcapslock
ceb7baf851 Fix choose selector active_choice when card editor config changes (#28858) 2026-01-08 10:20:42 +01:00
ildar170975
d195fd3244 Views: allow showing both icon & text title (#28690) 2026-01-07 19:03:48 +00:00
renovate[bot]
231cd632d6 Update dependency @bundle-stats/plugin-webpack-filter to v4.21.8 (#28846)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-07 17:23:37 +01:00
Wendelin
82d72ea39c Fix logs provider picker mobile width (#28847) 2026-01-07 16:30:49 +01:00
Wendelin
022bebb14f Throttle unknown value checks in ha-generic-picker (#28842) 2026-01-07 16:14:33 +01:00
Paul Bottein
0981ae1b4a Prefill the field with current value when editing a custom text item (#28840) 2026-01-07 15:42:47 +01:00
Paul Bottein
9608824a28 Remove ha-combo-box-textfield (#28841) 2026-01-07 15:39:58 +01:00
Marcin Bauer
33d215533e Add Shift+/ shortcut to shortcuts dialog and use Unicode command character (#28838)
* refactor: polish automation dialog UI and component styles

* Revert "Merge pull request #1 from marcinbauer85/fix/ui-polish-automation-dialog"

This reverts commit c2c47197e2, reversing
changes made to 49bed5e6a6.

* Add shortcuts dialog shortcut and use Unicode command character

* Update shortcut description text
2026-01-07 14:17:18 +00:00
Paul Bottein
5c503ecac0 Reduce shadow effect for scrollable fade mixin (#28832) 2026-01-07 14:16:41 +00:00
Wendelin
d114693fed Improve device picker performance (#28835) 2026-01-07 14:28:18 +01:00
Kristel
7a8cb80413 Add Voice assistant column to data tables (#28785)
* added Voice assistant column to data tables

* remove commented code

* fix column settings

* code review changes

* reuse voice-assistants-expose-assistant-icon

* refactor getEntityVoiceAssistantsKeys

* fix column width

* Apply suggestion from @MindFreeze

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-07 13:15:01 +00:00
Marcin Bauer
f5cd234c4b Refactor: Polish automation dialog UI and component styles (#28831)
* refactor: polish automation dialog UI and component styles

* Update ha-automation-row-targets.ts

- added borders to main automation list chips
2026-01-07 09:27:01 +00:00
karwosts
49bed5e6a6 Standardize all energy period calculations (#28827) 2026-01-07 08:46:54 +02: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
Paulus Schoutsen
4398e2d6c2 Remove snow flakes from hot path (#28716) 2025-12-27 20:28:58 +01:00
renovate[bot]
e95af3c661 Update dependency @codemirror/view to v6.39.7 (#28713)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-27 19:17:06 +01:00
renovate[bot]
53af746466 Update dependency @lit-labs/observers to v2.1.0 (#28712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-27 05:51:28 +00:00
renovate[bot]
18103c0e36 Update dependency lit-html to v3.3.2 (#28710)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-27 05:51:18 +00:00
renovate[bot]
d6d235d032 Update dependency @lit-labs/motion to v1.1.0 (#28711)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-27 06:41:31 +01:00
renovate[bot]
3761dec700 Update dependency lit to v3.3.2 (#28709)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-27 06:40:45 +01:00
renovate[bot]
606fa41e6e Update dependency @lit/reactive-element to v2.1.2 (#28708)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-27 06:40:32 +01:00
renovate[bot]
0ee37e9544 Update formatjs monorepo (#28706)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-26 20:48:03 +01:00
renovate[bot]
b0027b8c18 Update formatjs monorepo (#28704)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-26 16:32:59 +01:00
renovate[bot]
a397368e02 Update dependency @codemirror/view to v6.39.6 (#28702)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-26 14:15:09 +00:00
Joep Meindertsma
cac89e94df Add history link to statistics graph card (#28500) 2025-12-26 14:06:31 +00:00
Aidan Timson
cc2e001990 Migrate area and cropper dialogs to ha-wa-dialog (#28608)
* Migrate area dialog to ha-wa-dialog

* Migrate cropper dialog to wa
2025-12-26 11:40:41 +01:00
renovate[bot]
b1e2724aca Update dependency @rsdoctor/rspack-plugin to v1.4.0 (#28701)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-26 10:34:35 +00:00
ildar170975
5a60750841 ha-labels-picker: add a check for undefined label (#28686)
* add a check for undefined label

* simplify fix
2025-12-26 11:24:09 +01:00
ildar170975
94c180d64b state-badge element in Picture elements card: allow to set a "name" option (#28689)
* add "name" to StateBadgeElementConfig

* pass "name" to ha-state-label-badge

* add "name" field
2025-12-26 11:16:31 +01:00
ildar170975
6121f425c4 "Devices" & "Voice assistants expose": fix sort for "-" values (#28692)
* fix sorting for "-" values

* fix sorting for "-" values
2025-12-26 11:06:18 +01:00
renovate[bot]
91028fd0dd Update dependency typescript-eslint to v8.50.1 (#28698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-26 11:05:21 +01:00
renovate[bot]
082a2c08b9 Update fullcalendar monorepo to v6.1.20 (#28700)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-26 11:05:15 +01:00
renovate[bot]
d447dad28d Update CodeMirror (#28697)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-25 18:08:10 +01:00
renovate[bot]
4358ccdff8 Update CodeMirror (#28693)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-25 17:35:28 +02:00
Bram Kragten
8f7dd9b0ef Fix navigation in supervisor panel (#28683) 2025-12-24 13:32:30 +01:00
Paul Bottein
52db6a16d1 Add other devices view to home dashboard (#28097)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-12-24 09:38:47 +00:00
karwosts
ba213bf11c Don't set history redraw timer when not connected (#28679) 2025-12-24 10:08:35 +01:00
karwosts
5dea6b2c89 Fix a crash when clearing energy statistic (#28680) 2025-12-23 18:38:14 +01:00
Wendelin
97d51094df Add iOS-specific autofocus handling in HaWaDialog (#28607) 2025-12-23 14:43:03 +01:00
Wendelin
0904a1116c Language picker: add search fallback to en (#27818) 2025-12-23 14:41:30 +01:00
Wendelin
282458e645 Automation editor target in row improve configEntry subscription (#28662) 2025-12-23 14:40:55 +01:00
Paul Bottein
063c2d776a Improve new color picker (#28663)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-12-23 10:36:00 +00:00
Aidan Timson
97e0bc8080 Show icons for ha-tab in desktop views (#28508)
Add icons to tabs
2025-12-22 21:18:54 +01:00
renovate[bot]
21e2c676b8 Update dependency @lokalise/node-api to v15.6.0 (#28668)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 20:50:46 +01:00
Paul Bottein
214b8cd5c7 Put favorite at the top for home dashboard (#28665) 2025-12-22 20:50:22 +01:00
renovate[bot]
3bd5481274 Update formatjs monorepo (#28667)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 20:39:18 +01:00
Wendelin
ac63f991b2 Use generic-picker for log provider select (#28664)
Use generic-picker for log provider switcher
2025-12-22 18:56:22 +01:00
renovate[bot]
97e9129832 Update dependency sinon to v21.0.1 (#28666)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 18:02:51 +01:00
dependabot[bot]
704887999b Bump github/codeql-action from 4.31.8 to 4.31.9 (#28659)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.8 to 4.31.9.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](1b168cd394...5d4e8d1aca)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-22 12:19:52 +01:00
ndrwrbgs
3194fe9a30 Add back button to history when coming from history-card title link (#28649) 2025-12-22 09:38:16 +01:00
ndrwrbgs
5ce7308194 Change 'Weekdays' to 'Days of the Week' (#28656) 2025-12-21 21:28:46 +01:00
renovate[bot]
f9a9cf0ba0 Update dependency fs-extra to v11.3.3 (#28657)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-21 21:24:12 +01:00
renovate[bot]
bf90c6829f Update dependency @lokalise/node-api to v15.5.0 (#28653)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-21 21:23:57 +01:00
karwosts
36d4097ff8 Add select-all to media management dialog, design update, migrate to wa (#28595)
* Add select-all to media management dialog, design update

* Update src/components/media-player/dialog-media-manage.ts

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

* remove old styles

* css

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-21 21:22:40 +02:00
Aidan Timson
92bf8c3d47 Add missing close icon in adaptive dialog mode (#28461)
* Add missing close icon in adaptive dialog mode

* No need for default text

* Restore
2025-12-21 14:53:10 +00:00
renovate[bot]
4251f3468b Update dependency vite-tsconfig-paths to v6.0.3 (#28652)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-21 14:54:09 +01:00
karwosts
a6869e7c14 Use-top-label for statistic picker (#28639) 2025-12-20 18:25:42 +01:00
renovate[bot]
bd46c358fb Update dependency @codemirror/commands to v6.10.1 (#28642)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-20 10:59:12 +00:00
renovate[bot]
30b8ea1ae8 Update dependency @rspack/core to v1.6.8 (#28643)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-20 11:53:48 +01:00
renovate[bot]
a24dacf50d Update formatjs monorepo (#28641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-20 11:53:03 +01:00
renovate[bot]
7cbd07e33e Update dependency vite-tsconfig-paths to v6.0.2 (#28640)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-20 08:57:07 +01:00
renovate[bot]
c72ad83532 Lock file maintenance (#28621)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 19:11:00 +00:00
renovate[bot]
f2aba45dfe Update formatjs monorepo (major) (#28619)
* Update formatjs monorepo

* Add compiler paths

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2025-12-19 18:58:38 +00:00
Bram Kragten
639c2ce077 Add choose selector (#28624)
* Add choose selector

* add support for translation

* pass required

* Add to gallery
2025-12-19 19:52:31 +01:00
renovate[bot]
1bddc02ae0 Update dependency @formatjs/intl-durationformat to v0.8.1 (#28617)
* Update dependency @formatjs/intl-durationformat to v0.8.1

* Add compiler path for polyfill

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2025-12-19 18:33:11 +00:00
Simon
ebea5176e2 Add npmMinimalAgeGate to .yarnrc.yml (#28638) 2025-12-19 17:53:57 +00:00
Petar Petrov
39f550cf9f Fix datetime handling in energy charts (#28345)
* Fix datetime handling in energy charts

* PR comment

* Add detailedDailyData parameter to getSuggestedMax and update getCommonOptions

* refactor

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-12-19 18:47:18 +01:00
Paul Bottein
cdcbd00a92 Remove ha-space-0 (#28635)
* Remove ha-space-0

* Update src/components/ha-sidebar.ts

Co-authored-by: Aidan Timson <aidan@timmo.dev>

* Fix variable inside calc

* Replace for variables

---------

Co-authored-by: Aidan Timson <aidan@timmo.dev>
2025-12-19 17:12:28 +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
renovate[bot]
86cd0e81ad Update vitest monorepo to v4.0.16 (#28636)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 15:56:52 +00:00
Aidan Timson
6efe444af3 Use space tokens in more info dialog area (#28627)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-12-19 16:12:32 +01:00
Wendelin
09c9665b2f Remove ha-combo-box and vaadin dependencies (#28632) 2025-12-19 15:11:17 +01:00
Paul Bottein
23e394fec9 Always add favorite heading for home overview (#28629) 2025-12-19 11:35:44 +00:00
Wendelin
4b02a11634 Migrate ha-selector-select to use ha-generic-picker component (#28614) 2025-12-19 10:06:20 +00:00
Aidan Timson
ed9c00cab5 Migrate entity state picker to generic picker (#28613)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-12-19 10:52:39 +01:00
Aidan Timson
eaa1fb4107 Add space tokens to developer tools (#28626) 2025-12-19 09:33:53 +00:00
Aidan Timson
c0a49b3d0b Migrate floor dialog to webawesome (#28606) 2025-12-19 10:21:55 +01:00
karwosts
a7a00228a2 Add a debug tool to capture an entity diagnostic details (#28615) 2025-12-19 09:18:13 +00:00
renovate[bot]
c85f7a71b2 Update dependency @rsdoctor/rspack-plugin to v1.3.16 (#28623)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 09:11:31 +00:00
renovate[bot]
c95e914219 Update dependency @types/chromecast-caf-receiver to v6.0.25 (#27189)
* Update dependency @types/chromecast-caf-receiver to v6.0.25

* Fix typings

* Fix messageType type json

* Fix FMP4

* Add doc link

* Fix FMP4

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2025-12-19 10:53:48 +02:00
ildar170975
362a0b96ab ha-labels-picker: remove margin-bottom for ha-chip-set (#28559) 2025-12-19 08:55:21 +01:00
Jan Layola
ef984fc438 Trim whitespace from 2FA input before validation (#28616) 2025-12-19 08:48:11 +01:00
renovate[bot]
ce9bbc9972 Update dependency typescript-eslint to v8.50.0 (#28618)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 19:19:20 +01:00
Aidan Timson
a3921f0559 Migrate entity state content picker to generic picker (#28612)
* Migrate entity state content picker to generic picker

* Use similar primary/secondary as name picker

* Remove redundant func

* Memoize func

* Add custom value label

* Format

* Remove

* Remove renderer, use better translation

* Format

* Cleanup import

* Remove search labels where unused

* Merge
2025-12-18 19:12:03 +01:00
Aidan Timson
f8ec5d27a4 Migrate entity attribute picker to generic picker (#28611)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-12-18 17:03:21 +01:00
Wendelin
d679916fa5 Migrate domain selection to use generic picker component (#28605)
* Migrate domain selection to use generic picker component

* Remove unused CSS, add margins

* Space tokens

* Fix validation

---------

Co-authored-by: Aidan Timson <aidan@timmo.dev>
2025-12-18 13:39:16 +00:00
Aidan Timson
96be4768d3 Migrate entity name picker to generic picker (#28604)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-12-18 14:31:29 +01:00
Aidan Timson
6ff3b9f761 Migrate new label dialog to ha-wa-dialog (#28586) 2025-12-18 13:22:07 +01:00
Joakim Plate
9026009842 Let event domain expose attributes in gui (#28486) 2025-12-18 12:43:48 +01:00
Aidan Timson
54398a4784 Fix entity settings row sizing (#28585) 2025-12-18 12:42:29 +01:00
Wendelin
fa3cc970ec Migrate Z-Wave JS node configuration to use generic picker component (#28603) 2025-12-18 13:30:24 +02:00
Aidan Timson
1cf0560003 Migrate color picker to generic picker (#28598) 2025-12-18 11:23:25 +01:00
Wendelin
2a4ac15987 Generic-picker: Implement allowCustomValue (#28572)
* Introduce allowCustomValue and remove usage

* Review

* Fix secondary title
2025-12-18 12:19:57 +02:00
Aidan Timson
f264eebe49 Remove unused prop in target picker (#28601) 2025-12-18 10:14:42 +00:00
Aidan Timson
dae27e091f Migrate config entry picker to new picker syntax (#28600) 2025-12-18 10:10:55 +00:00
Aidan Timson
7ca681e417 Refactor generic pickers (#28570) 2025-12-18 10:53:50 +01:00
ndrwrbgs
1adfe63322 Add media query for prefers-reduced-motion in dialog styles (#28593)
Co-authored-by: ndrwrbgs <10776890+ndrwrbgs@users.noreply.github.com>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-12-18 08:18:19 +00:00
Wendelin
119a505a0d Add iOS focus element messaging for ha-generic-picker component (#28569) 2025-12-18 09:18:01 +01:00
renovate[bot]
1f8403f6c1 Update dependency vite-tsconfig-paths to v6 (#28596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 09:19:09 +02:00
Wendelin
7dd7309a47 Migrate addon-picker to generic-picker (#28567) 2025-12-17 16:33:41 +00:00
Wendelin
736afe2530 Migrate config-entry-picker to generic-picker (#28568)
* Use generic-picker for config-entry-picker

* Apply suggestion from @MindFreeze

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

* Apply suggestion from @MindFreeze

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

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-17 13:49:23 +02:00
Paul Bottein
92980dfddf Show summaries at top on mobile, sidebar on desktop (#28573)
* Use weather tile card and energy summary in home dashboard

* Only use sidebar on desktop

* Hide sidebar on mobile

* Rename widget to summaries

* Improve commonly used

* Feedbacks

* Use key instead of section
2025-12-17 11:45:27 +02:00
karwosts
9f3d6e1fea fix read-only fields in config flow expandables (#28579)
* fix read-only fields in config flow expandables

* types

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-17 07:29:56 +00:00
ildar170975
9e9adfcc90 ha-data-table: add ellipsis for ".secondary" (#28577)
add ellipsis for ".secondary"
2025-12-17 06:41:07 +01:00
karwosts
9b6ebdfcc0 device selector - add missing disabled & helper (#28576)
* device selector - add missing disabled & helper

* more
2025-12-17 06:39:50 +01:00
Aidan Timson
88faedba65 Migrate ha-icon-picker to generic picker (#27677) 2025-12-16 15:14:44 +00:00
Aidan Timson
b125cd5f3e Migrate hui-dialog-select-dashboard to ha-wa-dialog (#28456) 2025-12-16 13:47:50 +01:00
Aidan Timson
3425837de3 Switch energy now chart to watts, format values to W, kW etc (#28555)
* Switch energy now chart to watts

* Add kW

* Scale formatted value based on powers of 1000

1000 W -> 1 kW
W → kW → MW → GW → TW

* Explainers

* Use 3 dp for kW+ and 0 for W

* Add non-integer test
2025-12-16 14:33:45 +02:00
Paul Bottein
24a797e46a Improve shadow and border for energy date picker (#28566)
* Improve shadow and border for energy date picker

* Use variable

* Fix z-index

* Fix margin

* Improve mobile margin
2025-12-16 14:05:47 +02:00
Copilot
4a486ff28b Add fallback for empty cards in sections view (#28565)
* Initial plan

* Add edit-mode class to container in hui-grid-section to enable minimum card height

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>
2025-12-16 10:17:28 +00:00
Clément Notin
6be390a07e Add ignoreLocation option to fuseMultiTerm config (#28557) 2025-12-16 09:44:33 +01:00
Wendelin
c93942919b Automation editor show targets within rows (#28510)
* Automation editor show targets within rows

* review

* Fix expandable row icons

* Use state icon instead of state-badge

* Fix target wrap

* Use default font weight for automation rows

* Remove comma from targets in row
2025-12-16 09:42:38 +02:00
ildar170975
ca06269a91 Entity card: remove whitespaces in span (#28562) 2025-12-16 08:18:13 +01:00
ildar170975
b3d7c0b6dc Helpers table: show dashes in "Area" column (#28563) 2025-12-16 08:16:15 +01:00
Paul Bottein
e709703f79 Wait for translations before showing home and domain dashboards (#28556) 2025-12-16 08:11:15 +01:00
renovate[bot]
fd42614a23 Update dependency eslint to v9.39.2 (#28560)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 08:41:38 +02:00
Petar Petrov
4050dd0384 Fixed period selector for energy dashboard (#28458)
* Sticky period selector for energy dashboard

* Blur view when date picker is open

* move to hui-root via slot

* fix scrollbar

* Use dialog backdrop and define default

* Set energy selector position to fixed

* format

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2025-12-15 18:21:46 +01:00
Petar Petrov
9996b1bfea Fix inability to leave scene creation page without saving (#28546) 2025-12-15 16:59:57 +01:00
Petar Petrov
8092340d2a Fix min max issues in statistics chart (#28493) 2025-12-15 15:54:47 +01:00
Petar Petrov
443e205395 Fix power sources graph ordering with multiple sources (#28549) 2025-12-15 15:52:59 +01:00
Wendelin
8c5d0c4b80 Use ha-state-icon for add from target entity icon (#28551) 2025-12-15 15:51:17 +01:00
Paul Bottein
3005f12ef5 Restore navigation header for home, light, security and climate dashboards (#28552) 2025-12-15 15:44:51 +01:00
Petar Petrov
3bcf530200 Storage space breakdown chart (#28311)
Co-authored-by: Aidan Timson <aidan@timmo.dev>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-12-15 15:10:35 +01:00
ildar170975
af2fa98666 ha-map: add a variable for marker size (#28536)
* use a new variable for width & height

* use a new variable for width & height

* fix styles & creation of marker

* iconSize -> markerIconSize (for zones)

* Apply suggestion from @MindFreeze

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

* format

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-15 13:01:27 +00:00
renovate[bot]
8f335668db Update Node.js to v24 (#27687)
* Update Node.js to v24

* fix test

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-15 14:56:48 +02:00
Aidan Timson
51acd271ea Add duplicate voice assistant action (#28511) 2025-12-15 13:12:57 +01:00
dependabot[bot]
830e9b5089 Bump dessant/lock-threads from 5.0.1 to 6.0.0 (#28545)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 11:12:59 +00:00
dependabot[bot]
b13cb84baf Bump actions/upload-artifact from 5.0.0 to 6.0.0 (#28542)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 11:12:33 +00:00
renovate[bot]
f8df7f37ea Update dependency @codemirror/view to v6.39.4 (#28548)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 12:32:35 +02:00
Aidan Timson
54e4f4e60a Attempt to ensure view transitions are always ran (#28547) 2025-12-15 11:24:50 +01:00
ildar170975
81cb483163 Generic picker: show a label for area, category, language (#28236)
Co-authored-by: Aidan Timson <aidan@timmo.dev>
2025-12-15 08:54:12 +00:00
dependabot[bot]
eb5b14ea00 Bump actions/cache from 4.3.0 to 5.0.1 (#28543)
Bumps [actions/cache](https://github.com/actions/cache) from 4.3.0 to 5.0.1.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](0057852bfa...9255dc7a25)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 08:20:03 +02:00
renovate[bot]
b221fbb387 Update dependency @rsdoctor/rspack-plugin to v1.3.15 (#28541)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 06:11:43 +00:00
dependabot[bot]
3033bfb1fb Bump github/codeql-action from 4.31.7 to 4.31.8 (#28544)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.7 to 4.31.8.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](cf1bb45a27...1b168cd394)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 08:11:38 +02:00
karwosts
8db967eb2f Fix navigate action after confirm (#28535)
Fix navigate on confirm
2025-12-15 08:11:20 +02:00
renovate[bot]
10af86cc02 Update dependency terser-webpack-plugin to v5.3.16 (#28537)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 08:03:40 +02:00
renovate[bot]
dfc7116819 Update dependency @codemirror/view to v6.39.3 (#28533)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-14 15:56:09 +01:00
ildar170975
685c642bfc hui-map-card: add firstUpdated() with _getMapEntities() (#28526)
add firstUpdated() with _getMapEntities()
2025-12-13 17:15:57 +02:00
renovate[bot]
2e547937b8 Update dependency @rsdoctor/rspack-plugin to v1.3.13 (#28532)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-13 16:32:52 +02:00
Michael Bisbjerg
e3e01c327a Fix trace download truncation with Jinja comments (#28519) 2025-12-13 09:50:42 +02:00
renovate[bot]
abd706fed0 Update dependency @codemirror/view to v6.39.2 (#28522)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-13 07:59:22 +01:00
renovate[bot]
3bd45dd29b Update dependency ua-parser-js to v2.0.7 (#28516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 15:34:04 +01:00
renovate[bot]
206f067d2b Update dependency @rspack/core to v1.6.7 (#28518)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 15:31:53 +01:00
renovate[bot]
caefa7530a Update dependency @codemirror/view to v6.39.1 (#28520)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 15:31:26 +01:00
Aidan Timson
de142e33a1 Use entity picker for heading card entities editor (#28463)
* Use entity picker for heading card entities editor

* Use space tokens

* Pass index

* Lint

* Set undefined

* Iterate

* Spread

* Fixes

* Fixes
2025-12-12 09:30:07 +02:00
karwosts
5d4c3ebfcd Home strategy: don't link non-admin to config pages (#28512) 2025-12-12 07:23:37 +01:00
renovate[bot]
eb910c5ac5 Update dependency typescript-eslint to v8.49.0 (#28515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 07:23:10 +01:00
renovate[bot]
72726a2e0f Update dependency @codemirror/view to v6.39.0 (#28514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 07:22:53 +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
547 changed files with 26054 additions and 11809 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
@@ -154,7 +156,7 @@ try {
- **Use CSS custom properties**: Leverage the theme system
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
- Spacing scale: `--ha-space-0` (0px) through `--ha-space-20` (80px) in 4px increments
- Spacing scale: `--ha-space-1` (4px) through `--ha-space-20` (80px) in 4px increments
- Defined in `src/resources/theme/core.globals.ts`
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
- **Mobile-first responsive**: Design for mobile, enhance for desktop
@@ -619,7 +621,6 @@ this.hass.localize("ui.panel.config.updates.update_available", {
#### Key Terminology
- **"add-on"** (hyphenated, not "addon")
- **"integration"** (preferred over "component")
- **Technical terms**: Use lowercase (automation, entity, device, service)
@@ -711,7 +712,7 @@ this.hass.localize("ui.panel.config.automation.delete_confirm", {
- [ ] American English spelling
- [ ] Friendly, informational tone
- [ ] Avoids abbreviations and jargon
- [ ] Correct terminology (add-on not addon, integration not component)
- [ ] Correct terminology (integration not component)
### Component-Specific Checks

View File

@@ -26,7 +26,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -61,7 +61,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -26,7 +26,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: |
node_modules/.cache/prettier
@@ -60,7 +60,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -78,7 +78,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -89,11 +89,18 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: frontend-bundle-stats
path: build/stats/*.json
if-no-files-found: error
- name: Upload frontend build
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: frontend-build
path: hass_frontend/
if-no-files-found: error
retention-days: 7
supervisor:
name: Build supervisor
needs: [lint, test]
@@ -102,7 +109,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -113,7 +120,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: supervisor-bundle-stats
path: build/stats/*.json

View File

@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10

View File

@@ -27,7 +27,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -62,7 +62,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -9,7 +9,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
with:
github-token: ${{ github.token }}
process-only: "issues, prs"

View File

@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: translations
path: translations.tar.gz

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

@@ -19,8 +19,11 @@ jobs:
release:
name: Release
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: write # Required to upload release assets
id-token: write # For "Trusted Publisher" to PyPi
if: github.repository_owner == 'home-assistant'
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
@@ -34,7 +37,7 @@ jobs:
uses: home-assistant/actions/helpers/verify-version@master
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -46,14 +49,18 @@ jobs:
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build and release package
run: |
python3 -m pip install twine build
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
python3 -m pip install build
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
script/release
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
skip-existing: true
- name: Upload release assets
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
@@ -93,7 +100,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -122,7 +129,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".nvmrc"
cache: yarn

2
.gitignore vendored
View File

@@ -15,7 +15,7 @@ dist/
!.yarn/sdks
!.yarn/versions
.pnp.*
/node_modules/
node_modules/
yarn-error.log
npm-debug.log

2
.nvmrc
View File

@@ -1 +1 @@
22.21.1
24.13.0

View File

@@ -1,5 +1,7 @@
compressionLevel: mixed
npmMinimalAgeGate: "3d"
defaultSemverRangePrefix: ""
enableGlobalCache: false

View File

@@ -20,8 +20,6 @@ module.exports.ignorePackages = () => [];
// Files from NPM packages that we should replace with empty file
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
[
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
// Icons in supervisor conflict with icons in HA so we don't load.
(isHassioBuild || isLandingPageBuild) &&
require.resolve(

View File

@@ -168,12 +168,16 @@ const createRspackConfig = ({
);
},
}),
new rspack.NormalModuleReplacementPlugin(
new RegExp(
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).join("|")
),
path.resolve(paths.root_dir, "src/util/empty.js")
),
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).length
? new rspack.NormalModuleReplacementPlugin(
new RegExp(
bundle
.emptyPackages({ isHassioBuild, isLandingPageBuild })
.join("|")
),
path.resolve(paths.root_dir, "src/util/empty.js")
)
: false,
!isProdBuild && new LogStartCompilePlugin(),
isProdBuild &&
new StatsWriterPlugin({
@@ -201,6 +205,7 @@ const createRspackConfig = ({
"lit/decorators$": "lit/decorators.js",
"lit/directive$": "lit/directive.js",
"lit/directives/until$": "lit/directives/until.js",
"lit/directives/ref$": "lit/directives/ref.js",
"lit/directives/class-map$": "lit/directives/class-map.js",
"lit/directives/style-map$": "lit/directives/style-map.js",
"lit/directives/if-defined$": "lit/directives/if-defined.js",
@@ -209,7 +214,9 @@ const createRspackConfig = ({
"lit/directives/join$": "lit/directives/join.js",
"lit/directives/repeat$": "lit/directives/repeat.js",
"lit/directives/live$": "lit/directives/live.js",
"lit/directives/keyed$": "lit/directives/keyed.js",
"lit/directives/keyed$": latestBuild
? "lit/directives/keyed.js"
: path.resolve(__dirname, "../src/common/lit/keyed-es5.ts"),
"lit/polyfill-support$": "lit/polyfill-support.js",
"@lit-labs/virtualizer/layouts/grid":
"@lit-labs/virtualizer/layouts/grid.js",
@@ -217,6 +224,42 @@ const createRspackConfig = ({
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
"@lit-labs/observers/resize-controller":
"@lit-labs/observers/resize-controller.js",
"@formatjs/intl-durationformat/should-polyfill$":
"@formatjs/intl-durationformat/should-polyfill.js",
"@formatjs/intl-durationformat/polyfill-force$":
"@formatjs/intl-durationformat/polyfill-force.js",
"@formatjs/intl-datetimeformat/should-polyfill":
"@formatjs/intl-datetimeformat/should-polyfill.js",
"@formatjs/intl-datetimeformat/polyfill-force":
"@formatjs/intl-datetimeformat/polyfill-force.js",
"@formatjs/intl-displaynames/should-polyfill":
"@formatjs/intl-displaynames/should-polyfill.js",
"@formatjs/intl-displaynames/polyfill-force":
"@formatjs/intl-displaynames/polyfill-force.js",
"@formatjs/intl-getcanonicallocales/should-polyfill":
"@formatjs/intl-getcanonicallocales/should-polyfill.js",
"@formatjs/intl-getcanonicallocales/polyfill-force":
"@formatjs/intl-getcanonicallocales/polyfill-force.js",
"@formatjs/intl-listformat/should-polyfill":
"@formatjs/intl-listformat/should-polyfill.js",
"@formatjs/intl-listformat/polyfill-force":
"@formatjs/intl-listformat/polyfill-force.js",
"@formatjs/intl-locale/should-polyfill":
"@formatjs/intl-locale/should-polyfill.js",
"@formatjs/intl-locale/polyfill-force":
"@formatjs/intl-locale/polyfill-force.js",
"@formatjs/intl-numberformat/should-polyfill":
"@formatjs/intl-numberformat/should-polyfill.js",
"@formatjs/intl-numberformat/polyfill-force":
"@formatjs/intl-numberformat/polyfill-force.js",
"@formatjs/intl-pluralrules/should-polyfill":
"@formatjs/intl-pluralrules/should-polyfill.js",
"@formatjs/intl-pluralrules/polyfill-force":
"@formatjs/intl-pluralrules/polyfill-force.js",
"@formatjs/intl-relativetimeformat/should-polyfill":
"@formatjs/intl-relativetimeformat/should-polyfill.js",
"@formatjs/intl-relativetimeformat/polyfill-force":
"@formatjs/intl-relativetimeformat/polyfill-force.js",
},
},
output: {

View File

@@ -5,17 +5,19 @@ const castContext = framework.CastReceiverContext.getInstance();
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
framework.messages.MessageType.LOAD,
"LOAD" as framework.messages.MessageType.LOAD,
(loadRequestData) => {
const media = loadRequestData.media;
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = framework.messages.StreamType.LIVE;
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
// type definition is wrong, should be "FMP4" instead of "fmp4"
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
media.hlsVideoSegmentFormat =
framework.messages.HlsVideoSegmentFormat.FMP4;
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}

View File

@@ -1,10 +1,9 @@
import { framework } from "./cast_framework";
import { CAST_NS } from "../../../src/cast/const";
import type { HassMessage } from "../../../src/cast/receiver_messages";
import "../../../src/resources/custom-card-support";
import { castContext } from "./cast_context";
import { framework } from "./cast_framework";
import { HcMain } from "./layout/hc-main";
import type { ReceivedMessage } from "./types";
const lovelaceController = new HcMain();
document.body.append(lovelaceController);
@@ -40,7 +39,8 @@ const playDummyMedia = (viewTitle?: string) => {
loadRequestData.media.contentId =
"https://cast.home-assistant.io/images/google-nest-hub.png";
loadRequestData.media.contentType = "image/jpeg";
loadRequestData.media.streamType = framework.messages.StreamType.NONE;
loadRequestData.media.streamType =
"NONE" as framework.messages.StreamType.NONE;
const metadata = new framework.messages.GenericMediaMetadata();
metadata.title = viewTitle;
loadRequestData.media.metadata = metadata;
@@ -89,31 +89,30 @@ const showMediaPlayer = () => {
const options = new framework.CastReceiverOptions();
options.disableIdleTimeout = true;
options.customNamespaces = {
[CAST_NS]: framework.system.MessageType.JSON,
// type definition is wrong, should be "JSON" instead of "json"
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.system#.MessageType
[CAST_NS]: "JSON" as framework.system.MessageType.JSON,
};
castContext.addCustomMessageListener(
CAST_NS,
// @ts-ignore
(ev: ReceivedMessage<HassMessage>) => {
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
if (
playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE
) {
playerManager.stop();
} else {
showLovelaceController();
}
const msg = ev.data;
msg.senderId = ev.senderId;
lovelaceController.processIncomingMessage(msg);
castContext.addCustomMessageListener(CAST_NS, (ev) => {
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
if (
playerManager.getPlayerState() !==
("IDLE" as framework.messages.PlayerState.IDLE)
) {
playerManager.stop();
} else {
showLovelaceController();
}
);
const msg = ev.data as HassMessage;
msg.senderId = ev.senderId;
lovelaceController.processIncomingMessage(msg);
});
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
framework.messages.MessageType.LOAD,
"LOAD" as framework.messages.MessageType.LOAD,
(loadRequestData) => {
if (
loadRequestData.media.contentId ===
@@ -127,24 +126,26 @@ playerManager.setMessageInterceptor(
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = framework.messages.StreamType.LIVE;
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
// type definition is wrong, should be "FMP4" instead of "fmp4"
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
media.hlsVideoSegmentFormat =
framework.messages.HlsVideoSegmentFormat.FMP4;
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}
);
playerManager.addEventListener(
framework.events.EventType.MEDIA_STATUS,
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS,
(event) => {
if (
event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
event.mediaStatus?.playerState ===
("IDLE" as framework.messages.PlayerState.IDLE) &&
event.mediaStatus?.idleReason &&
event.mediaStatus?.idleReason !==
framework.messages.IdleReason.INTERRUPTED
("INTERRUPTED" as framework.messages.IdleReason.INTERRUPTED)
) {
// media finished or stopped, return to default Lovelace
showLovelaceController();

View File

@@ -1,6 +0,0 @@
export interface ReceivedMessage<T> {
gj: boolean;
data: T;
senderId: string;
type: "message";
}

View File

@@ -1,4 +1,4 @@
import type { AreaRegistryEntry } from "../../../src/data/area_registry";
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockAreaRegistry = (

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

@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action">
<span>
${this._action
? describeAction(this.hass, [], [], {}, this._action)
? describeAction(this.hass, [], this._action)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, [], [], {}, conf as any)}</span>
<span>${describeAction(this.hass, [], conf as any)}</span>
<pre>${dump(conf)}</pre>
</div>
`

View File

@@ -10,7 +10,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
@@ -169,7 +169,7 @@ const SCHEMAS: {
{
title: "Selectors",
translations: {
addon: "Addon",
addon: "App",
entity: "Entity",
device: "Device",
area: "Area",

View File

@@ -11,7 +11,7 @@ import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-selector/ha-selector";
import "../../../../src/components/ha-settings-row";
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
import type { BlueprintInput } from "../../../../src/data/blueprint";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
@@ -40,6 +40,9 @@ const ENTITIES = [
getEntity("switch", "coffee", "off", {
friendly_name: "Coffee",
}),
getEntity("number", "number", 5, {
friendly_name: "Number",
}),
];
const DEVICES: DeviceRegistryEntry[] = [
@@ -236,7 +239,7 @@ const SCHEMAS: {
selector: { config_entry: {} },
},
duration: { name: "Duration", selector: { duration: {} } },
addon: { name: "Addon", selector: { addon: {} } },
addon: { name: "App", selector: { addon: {} } },
number_box: {
name: "Number Box",
selector: {
@@ -377,6 +380,33 @@ const SCHEMAS: {
name: "Constant",
selector: { constant: { value: true, label: "Yes!" } },
},
choose: {
name: "Choose",
selector: {
choose: {
choices: {
number: {
selector: {
number: {
min: 0,
max: 100,
step: 0.1,
},
},
},
entity: {
selector: {
entity: {
filter: {
domain: "number",
},
},
},
},
},
},
},
},
},
},
{

View File

@@ -83,10 +83,10 @@ export class HassioAddonRepositoryEl extends LitElement {
? this.supervisor.localize(
"common.new_version_available"
)
: this.supervisor.localize("addon.state.installed")
: this.supervisor.localize("app.state.installed")
: addon.available
? this.supervisor.localize("addon.state.not_installed")
: this.supervisor.localize("addon.state.not_available")}
? this.supervisor.localize("app.state.not_installed")
: this.supervisor.localize("app.state.not_available")}
.iconClass=${addon.installed
? addon.update_available
? "update"

View File

@@ -120,7 +120,7 @@ export class HassioAddonStore extends LitElement {
? html`
<div class="advanced">
<a href="/profile" target="_top">
${this.supervisor.localize("store.missing_addons")}
${this.supervisor.localize("store.missing_apps")}
</a>
</div>
`

View File

@@ -44,7 +44,7 @@ class HassioAddonAudio extends LitElement {
return html`
<ha-card
outlined
.header=${this.supervisor.localize("addon.configuration.audio.header")}
.header=${this.supervisor.localize("app.configuration.audio.header")}
>
<div class="card-content">
${this._error
@@ -52,9 +52,7 @@ class HassioAddonAudio extends LitElement {
: nothing}
${this._inputDevices &&
html`<ha-select
.label=${this.supervisor.localize(
"addon.configuration.audio.input"
)}
.label=${this.supervisor.localize("app.configuration.audio.input")}
@selected=${this._setInputDevice}
@closed=${stopPropagation}
fixedMenuPosition
@@ -72,9 +70,7 @@ class HassioAddonAudio extends LitElement {
</ha-select>`}
${this._outputDevices &&
html`<ha-select
.label=${this.supervisor.localize(
"addon.configuration.audio.output"
)}
.label=${this.supervisor.localize("app.configuration.audio.output")}
@selected=${this._setOutputDevice}
@closed=${stopPropagation}
fixedMenuPosition
@@ -153,7 +149,7 @@ class HassioAddonAudio extends LitElement {
const noDevice: HassioHardwareAudioDevice = {
device: "default",
name: this.supervisor.localize("addon.configuration.audio.default"),
name: this.supervisor.localize("app.configuration.audio.default"),
};
try {

View File

@@ -81,7 +81,7 @@ class HassioAddonConfigDashboard extends LitElement {
`
: nothing}
`
: this.supervisor.localize("addon.configuration.no_configuration")}
: this.supervisor.localize("app.configuration.no_configuration")}
</div>
`;
}

View File

@@ -219,7 +219,7 @@ class HassioAddonConfig extends LitElement {
<ha-card outlined>
<div class="header">
<h2>
${this.supervisor.localize("addon.configuration.options.header")}
${this.supervisor.localize("app.configuration.options.header")}
</h2>
<div class="card-menu">
<ha-button-menu @action=${this._handleAction}>
@@ -231,10 +231,10 @@ class HassioAddonConfig extends LitElement {
<ha-list-item .disabled=${!this._canShowSchema || this.disabled}>
${this._yamlMode
? this.supervisor.localize(
"addon.configuration.options.edit_in_ui"
"app.configuration.options.edit_in_ui"
)
: this.supervisor.localize(
"addon.configuration.options.edit_in_yaml"
"app.configuration.options.edit_in_yaml"
)}
</ha-list-item>
<ha-list-item
@@ -279,7 +279,7 @@ class HassioAddonConfig extends LitElement {
: html`
<ha-alert alert-type="error">
${this.supervisor.localize(
"addon.configuration.options.invalid_yaml"
"app.configuration.options.invalid_yaml"
)}
</ha-alert>
`}
@@ -288,7 +288,7 @@ class HassioAddonConfig extends LitElement {
? html`<ha-formfield
class="show-additional"
.label=${this.supervisor.localize(
"addon.configuration.options.show_unused_optional"
"app.configuration.options.show_unused_optional"
)}
>
<ha-switch
@@ -397,7 +397,7 @@ class HassioAddonConfig extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
this._error = this.supervisor.localize("addon.failed_to_reset", {
this._error = this.supervisor.localize("app.failed_to_reset", {
error: extractApiErrorMessage(err),
});
}
@@ -440,7 +440,7 @@ class HassioAddonConfig extends LitElement {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch (err: any) {
this._error = this.supervisor.localize("addon.failed_to_save", {
this._error = this.supervisor.localize("app.failed_to_save", {
error: extractApiErrorMessage(err),
});
eventdata.success = false;

View File

@@ -56,14 +56,12 @@ class HassioAddonNetwork extends LitElement {
return html`
<ha-card
outlined
.header=${this.supervisor.localize(
"addon.configuration.network.header"
)}
.header=${this.supervisor.localize("app.configuration.network.header")}
>
<div class="card-content">
<p>
${this.supervisor.localize(
"addon.configuration.network.introduction"
"app.configuration.network.introduction"
)}
</p>
${this._error
@@ -87,7 +85,7 @@ class HassioAddonNetwork extends LitElement {
? html`<ha-formfield
class="show-optional"
.label=${this.supervisor.localize(
"addon.configuration.network.show_disabled"
"app.configuration.network.show_disabled"
)}
>
<ha-switch
@@ -187,7 +185,7 @@ class HassioAddonNetwork extends LitElement {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch (err: any) {
this._error = this.supervisor.localize("addon.failed_to_reset", {
this._error = this.supervisor.localize("app.failed_to_reset", {
error: extractApiErrorMessage(err),
});
button.actionError();
@@ -229,7 +227,7 @@ class HassioAddonNetwork extends LitElement {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch (err: any) {
this._error = this.supervisor.localize("addon.failed_to_save", {
this._error = this.supervisor.localize("app.failed_to_save", {
error: extractApiErrorMessage(err),
});
button.actionError();

View File

@@ -83,7 +83,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
);
} catch (err: any) {
this._error = this.supervisor.localize(
"addon.documentation.get_documentation",
"app.documentation.get_documentation",
{ error: extractApiErrorMessage(err) }
);
}

View File

@@ -2,7 +2,7 @@ import {
mdiCogs,
mdiFileDocument,
mdiInformationVariant,
mdiMathLog,
mdiTextBoxOutline,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
@@ -89,7 +89,7 @@ class HassioAddonDashboard extends LitElement {
const addonTabs: PageNavigation[] = [
{
translationKey: "addon.panel.info",
translationKey: "app.panel.info",
path: `/hassio/addon/${this.addon.slug}/info`,
iconPath: mdiInformationVariant,
},
@@ -97,7 +97,7 @@ class HassioAddonDashboard extends LitElement {
if (this.addon.documentation) {
addonTabs.push({
translationKey: "addon.panel.documentation",
translationKey: "app.panel.documentation",
path: `/hassio/addon/${this.addon.slug}/documentation`,
iconPath: mdiFileDocument,
});
@@ -106,14 +106,14 @@ class HassioAddonDashboard extends LitElement {
if (this.addon.version) {
addonTabs.push(
{
translationKey: "addon.panel.configuration",
translationKey: "app.panel.configuration",
path: `/hassio/addon/${this.addon.slug}/config`,
iconPath: mdiCogs,
},
{
translationKey: "addon.panel.log",
translationKey: "app.panel.log",
path: `/hassio/addon/${this.addon.slug}/logs`,
iconPath: mdiMathLog,
iconPath: mdiTextBoxOutline,
}
);
}
@@ -195,10 +195,10 @@ class HassioAddonDashboard extends LitElement {
) {
if (
!(await showConfirmationDialog(this, {
title: this.supervisor.localize("my.add_addon_repository_title"),
title: this.supervisor.localize("my.add_app_repository_title"),
text: this.supervisor.localize(
"my.add_addon_repository_description",
{ addon: requestedAddon, repository: requestedAddonRepository }
"my.add_app_repository_description",
{ app: requestedAddon, repository: requestedAddonRepository }
),
confirmText: this.supervisor.localize("common.add"),
dismissText: this.supervisor.localize("common.cancel"),
@@ -224,7 +224,7 @@ class HassioAddonDashboard extends LitElement {
(addon) => addon.slug === requestedAddon
);
if (!validAddon) {
this._error = this.supervisor.localize("my.error_addon_not_found");
this._error = this.supervisor.localize("my.error_app_not_found");
} else {
navigate(`/hassio/addon/${requestedAddon}`, { replace: true });
}

View File

@@ -150,11 +150,11 @@ class HassioAddonInfo extends LitElement {
: undefined;
const metrics = [
{
description: this.supervisor.localize("addon.dashboard.cpu_usage"),
description: this.supervisor.localize("app.dashboard.cpu_usage"),
value: this._metrics?.cpu_percent,
},
{
description: this.supervisor.localize("addon.dashboard.ram_usage"),
description: this.supervisor.localize("app.dashboard.ram_usage"),
value: this._metrics?.memory_percent,
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
this._metrics?.memory_limit
@@ -181,11 +181,11 @@ class HassioAddonInfo extends LitElement {
<ha-alert
alert-type="error"
.title=${this.supervisor.localize(
"addon.dashboard.protection_mode.title"
"app.dashboard.protection_mode.title"
)}
>
${this.supervisor.localize(
"addon.dashboard.protection_mode.content"
"app.dashboard.protection_mode.content"
)}
<ha-button
variant="danger"
@@ -193,7 +193,7 @@ class HassioAddonInfo extends LitElement {
@click=${this._protectionToggled}
>
${this.supervisor.localize(
"addon.dashboard.protection_mode.enable"
"app.dashboard.protection_mode.enable"
)}
</ha-button>
</ha-alert>
@@ -220,7 +220,7 @@ class HassioAddonInfo extends LitElement {
? html`
<ha-svg-icon
.title=${this.supervisor.localize(
"dashboard.addon_running"
"dashboard.app_running"
)}
class="running"
.path=${mdiPlayCircle}
@@ -229,7 +229,7 @@ class HassioAddonInfo extends LitElement {
: html`
<ha-svg-icon
.title=${this.supervisor.localize(
"dashboard.addon_stopped"
"dashboard.app_stopped"
)}
class="stopped"
.path=${mdiCircleOffOutline}
@@ -242,22 +242,19 @@ class HassioAddonInfo extends LitElement {
<div class="description light-color">
${this.addon.version
? html`
${this.supervisor.localize(
"addon.dashboard.current_version",
{ version: this.addon.version }
)}
${this.supervisor.localize("app.dashboard.current_version", {
version: this.addon.version,
})}
<div class="changelog" @click=${this._openChangelog}>
(<span class="changelog-link"
>${this.supervisor.localize(
"addon.dashboard.changelog"
"app.dashboard.changelog"
)}</span
>)
</div>
`
: html`<span class="changelog-link" @click=${this._openChangelog}
>${this.supervisor.localize(
"addon.dashboard.changelog"
)}</span
>${this.supervisor.localize("app.dashboard.changelog")}</span
>`}
</div>
@@ -274,7 +271,7 @@ class HassioAddonInfo extends LitElement {
id="stage"
.label=${capitalizeFirstLetter(
this.supervisor.localize(
`addon.dashboard.capability.stages.${this.addon.stage}`
`app.dashboard.capability.stages.${this.addon.stage}`
)
)}
>
@@ -298,7 +295,7 @@ class HassioAddonInfo extends LitElement {
id="rating"
.label=${capitalizeFirstLetter(
this.supervisor.localize(
"addon.dashboard.capability.label.rating"
"app.dashboard.capability.label.rating"
)
)}
>
@@ -313,7 +310,7 @@ class HassioAddonInfo extends LitElement {
id="host_network"
.label=${capitalizeFirstLetter(
this.supervisor.localize(
"addon.dashboard.capability.label.host"
"app.dashboard.capability.label.host"
)
)}
>
@@ -329,7 +326,7 @@ class HassioAddonInfo extends LitElement {
id="full_access"
.label=${capitalizeFirstLetter(
this.supervisor.localize(
"addon.dashboard.capability.label.hardware"
"app.dashboard.capability.label.hardware"
)
)}
>
@@ -345,7 +342,7 @@ class HassioAddonInfo extends LitElement {
id="homeassistant_api"
.label=${capitalizeFirstLetter(
this.supervisor.localize(
"addon.dashboard.capability.label.core"
"app.dashboard.capability.label.core"
)
)}
>
@@ -364,7 +361,7 @@ class HassioAddonInfo extends LitElement {
id="hassio_api"
.label=${capitalizeFirstLetter(
this.supervisor.localize(
`addon.dashboard.capability.role.${this.addon.hassio_role}`
`app.dashboard.capability.role.${this.addon.hassio_role}`
) || this.addon.hassio_role
)}
>
@@ -383,7 +380,7 @@ class HassioAddonInfo extends LitElement {
id="docker_api"
.label=${capitalizeFirstLetter(
this.supervisor.localize(
"addon.dashboard.capability.label.docker"
"app.dashboard.capability.label.docker"
)
)}
>
@@ -399,7 +396,7 @@ class HassioAddonInfo extends LitElement {
id="host_pid"
.label=${capitalizeFirstLetter(
this.supervisor.localize(
"addon.dashboard.capability.label.host_pid"
"app.dashboard.capability.label.host_pid"
)
)}
>
@@ -416,7 +413,7 @@ class HassioAddonInfo extends LitElement {
id="apparmor"
.label=${capitalizeFirstLetter(
this.supervisor.localize(
"addon.dashboard.capability.label.apparmor"
"app.dashboard.capability.label.apparmor"
)
)}
>
@@ -432,7 +429,7 @@ class HassioAddonInfo extends LitElement {
id="auth_api"
.label=${capitalizeFirstLetter(
this.supervisor.localize(
"addon.dashboard.capability.label.auth"
"app.dashboard.capability.label.auth"
)
)}
>
@@ -448,7 +445,7 @@ class HassioAddonInfo extends LitElement {
id="ingress"
.label=${capitalizeFirstLetter(
this.supervisor.localize(
"addon.dashboard.capability.label.ingress"
"app.dashboard.capability.label.ingress"
)
)}
>
@@ -467,7 +464,7 @@ class HassioAddonInfo extends LitElement {
id="signed"
.label=${capitalizeFirstLetter(
this.supervisor.localize(
"addon.dashboard.capability.label.signed"
"app.dashboard.capability.label.signed"
)
)}
>
@@ -482,7 +479,7 @@ class HassioAddonInfo extends LitElement {
@click=${this._showSystemManagedDialog}
id="system_managed"
.label=${capitalizeFirstLetter(
this.supervisor.localize("addon.system_managed.badge")
this.supervisor.localize("app.system_managed.badge")
)}
>
<ha-svg-icon
@@ -496,7 +493,7 @@ class HassioAddonInfo extends LitElement {
<div class="description light-color">
${this.addon.description}.<br />
${this.supervisor.localize("addon.dashboard.visit_addon_page", {
${this.supervisor.localize("app.dashboard.visit_app_page", {
name: html`<a
href=${this.addon.url!}
target="_blank"
@@ -527,12 +524,12 @@ class HassioAddonInfo extends LitElement {
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.supervisor.localize(
"addon.dashboard.option.boot.title"
"app.dashboard.option.boot.title"
)}
</span>
<span slot="description">
${this.supervisor.localize(
"addon.dashboard.option.boot.description"
"app.dashboard.option.boot.description"
)}
</span>
<ha-switch
@@ -548,12 +545,12 @@ class HassioAddonInfo extends LitElement {
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.supervisor.localize(
"addon.dashboard.option.watchdog.title"
"app.dashboard.option.watchdog.title"
)}
</span>
<span slot="description">
${this.supervisor.localize(
"addon.dashboard.option.watchdog.description"
"app.dashboard.option.watchdog.description"
)}
</span>
<ha-switch
@@ -572,12 +569,12 @@ class HassioAddonInfo extends LitElement {
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.supervisor.localize(
"addon.dashboard.option.auto_update.title"
"app.dashboard.option.auto_update.title"
)}
</span>
<span slot="description">
${this.supervisor.localize(
"addon.dashboard.option.auto_update.description"
"app.dashboard.option.auto_update.description"
)}
</span>
<ha-switch
@@ -595,12 +592,12 @@ class HassioAddonInfo extends LitElement {
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.supervisor.localize(
"addon.dashboard.option.ingress_panel.title"
"app.dashboard.option.ingress_panel.title"
)}
</span>
<span slot="description">
${this.supervisor.localize(
"addon.dashboard.option.ingress_panel.description"
"app.dashboard.option.ingress_panel.description"
)}
</span>
<ha-switch
@@ -618,12 +615,12 @@ class HassioAddonInfo extends LitElement {
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.supervisor.localize(
"addon.dashboard.option.protected.title"
"app.dashboard.option.protected.title"
)}
</span>
<span slot="description">
${this.supervisor.localize(
"addon.dashboard.option.protected.description"
"app.dashboard.option.protected.description"
)}
</span>
<ha-switch
@@ -644,7 +641,7 @@ class HassioAddonInfo extends LitElement {
${this.addon.version && this.addon.state === "started"
? html`<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.supervisor.localize("addon.dashboard.hostname")}
${this.supervisor.localize("app.dashboard.hostname")}
</span>
<code slot="description"> ${this.addon.hostname} </code>
</ha-settings-row>
@@ -671,14 +668,14 @@ class HassioAddonInfo extends LitElement {
? html`
<ha-alert alert-type="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch"
"app.dashboard.not_available_arch"
)}
</ha-alert>
`
: html`
<ha-alert alert-type="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_version",
"app.dashboard.not_available_version",
{
core_version_installed: this.supervisor.core.version,
core_version_needed: addonStoreInfo!.homeassistant,
@@ -699,14 +696,14 @@ class HassioAddonInfo extends LitElement {
@click=${this._stopClicked}
.disabled=${systemManaged && !this.controlEnabled}
>
${this.supervisor.localize("addon.dashboard.stop")}
${this.supervisor.localize("app.dashboard.stop")}
</ha-progress-button>
<ha-progress-button
variant="danger"
appearance="plain"
@click=${this._restartClicked}
>
${this.supervisor.localize("addon.dashboard.restart")}
${this.supervisor.localize("app.dashboard.restart")}
</ha-progress-button>
`
: html`
@@ -715,7 +712,7 @@ class HassioAddonInfo extends LitElement {
.progress=${this.addon.state === "startup"}
appearance="plain"
>
${this.supervisor.localize("addon.dashboard.start")}
${this.supervisor.localize("app.dashboard.start")}
</ha-progress-button>
`
: nothing}
@@ -729,7 +726,7 @@ class HassioAddonInfo extends LitElement {
@click=${this._uninstallClicked}
.disabled=${systemManaged && !this.controlEnabled}
>
${this.supervisor.localize("addon.dashboard.uninstall")}
${this.supervisor.localize("app.dashboard.uninstall")}
</ha-progress-button>
${this.addon.build
? html`
@@ -738,7 +735,7 @@ class HassioAddonInfo extends LitElement {
appearance="plain"
@click=${this._rebuildClicked}
>
${this.supervisor.localize("addon.dashboard.rebuild")}
${this.supervisor.localize("app.dashboard.rebuild")}
</ha-progress-button>
`
: nothing}
@@ -761,7 +758,7 @@ class HassioAddonInfo extends LitElement {
: undefined}
>
${this.supervisor.localize(
"addon.dashboard.open_web_ui"
"app.dashboard.open_web_ui"
)}
</ha-button>
`
@@ -772,7 +769,7 @@ class HassioAddonInfo extends LitElement {
.disabled=${!this.addon.available}
@click=${this._installClicked}
>
${this.supervisor.localize("addon.dashboard.install")}
${this.supervisor.localize("app.dashboard.install")}
</ha-progress-button>
`}
</div>
@@ -804,7 +801,7 @@ class HassioAddonInfo extends LitElement {
"state" in this.addon &&
this.addon.state === "startup"
) {
// Addon is starting up, wait for it to start
// App is starting up, wait for it to start
this._scheduleDataUpdate();
}
}
@@ -858,11 +855,11 @@ class HassioAddonInfo extends LitElement {
private _showMoreInfo(ev): void {
const id = ev.currentTarget.id as AddonCapability;
showHassioMarkdownDialog(this, {
title: this.supervisor.localize(`addon.dashboard.capability.${id}.title`),
title: this.supervisor.localize(`app.dashboard.capability.${id}.title`),
content:
id === "stage"
? this.supervisor.localize(
`addon.dashboard.capability.${id}.description`,
`app.dashboard.capability.${id}.description`,
{
icon_stable: `<ha-svg-icon path="${STAGE_ICON.stable}"></ha-svg-icon>`,
icon_experimental: `<ha-svg-icon path="${STAGE_ICON.experimental}"></ha-svg-icon>`,
@@ -870,7 +867,7 @@ class HassioAddonInfo extends LitElement {
}
)
: this.supervisor.localize(
`addon.dashboard.capability.${id}.description`
`app.dashboard.capability.${id}.description`
),
});
}
@@ -936,7 +933,7 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
this._error = this.supervisor.localize("addon.failed_to_save", {
this._error = this.supervisor.localize("app.failed_to_save", {
error: extractApiErrorMessage(err),
});
}
@@ -956,7 +953,7 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
this._error = this.supervisor.localize("addon.failed_to_save", {
this._error = this.supervisor.localize("app.failed_to_save", {
error: extractApiErrorMessage(err),
});
}
@@ -976,7 +973,7 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
this._error = this.supervisor.localize("addon.failed_to_save", {
this._error = this.supervisor.localize("app.failed_to_save", {
error: extractApiErrorMessage(err),
});
}
@@ -996,7 +993,7 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
this._error = this.supervisor.localize("addon.failed_to_save", {
this._error = this.supervisor.localize("app.failed_to_save", {
error: extractApiErrorMessage(err),
});
}
@@ -1016,7 +1013,7 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
this._error = this.supervisor.localize("addon.failed_to_save", {
this._error = this.supervisor.localize("app.failed_to_save", {
error: extractApiErrorMessage(err),
});
}
@@ -1030,13 +1027,13 @@ class HassioAddonInfo extends LitElement {
);
showHassioMarkdownDialog(this, {
title: this.supervisor.localize("addon.dashboard.changelog"),
title: this.supervisor.localize("app.dashboard.changelog"),
content: extractChangelog(this.addon as HassioAddonDetails, content),
});
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize(
"addon.dashboard.action_error.get_changelog"
"app.dashboard.action_error.get_changelog"
),
text: extractApiErrorMessage(err),
});
@@ -1066,7 +1063,7 @@ class HassioAddonInfo extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("addon.dashboard.action_error.install"),
title: this.supervisor.localize("app.dashboard.action_error.install"),
text: extractApiErrorMessage(err),
});
}
@@ -1091,7 +1088,7 @@ class HassioAddonInfo extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("addon.dashboard.action_error.stop"),
title: this.supervisor.localize("app.dashboard.action_error.stop"),
text: extractApiErrorMessage(err),
});
}
@@ -1112,7 +1109,7 @@ class HassioAddonInfo extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("addon.dashboard.action_error.restart"),
title: this.supervisor.localize("app.dashboard.action_error.restart"),
text: extractApiErrorMessage(err),
});
}
@@ -1127,7 +1124,7 @@ class HassioAddonInfo extends LitElement {
await rebuildLocalAddon(this.hass, this.addon.slug);
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("addon.dashboard.action_error.rebuild"),
title: this.supervisor.localize("app.dashboard.action_error.rebuild"),
text: extractApiErrorMessage(err),
});
}
@@ -1145,12 +1142,12 @@ class HassioAddonInfo extends LitElement {
if (!validate.valid) {
await showConfirmationDialog(this, {
title: this.supervisor.localize(
"addon.dashboard.action_error.start_invalid_config"
"app.dashboard.action_error.start_invalid_config"
),
text: validate.message.split(" Got ")[0],
confirm: () => this._openConfiguration(),
confirmText: this.supervisor.localize(
"addon.dashboard.action_error.go_to_config"
"app.dashboard.action_error.go_to_config"
),
dismissText: this.supervisor.localize("common.cancel"),
});
@@ -1162,7 +1159,7 @@ class HassioAddonInfo extends LitElement {
button.actionError();
button.progress = false;
showAlertDialog(this, {
title: "Failed to validate addon configuration",
title: "Failed to validate app configuration",
text: extractApiErrorMessage(err),
});
return;
@@ -1181,7 +1178,7 @@ class HassioAddonInfo extends LitElement {
button.actionError();
button.progress = false;
showAlertDialog(this, {
title: this.supervisor.localize("addon.dashboard.action_error.start"),
title: this.supervisor.localize("app.dashboard.action_error.start"),
text: extractApiErrorMessage(err),
});
return;
@@ -1207,13 +1204,13 @@ class HassioAddonInfo extends LitElement {
};
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("dialog.uninstall_addon.title", {
title: this.supervisor.localize("dialog.uninstall_app.title", {
name: this.addon.name,
}),
text: html`
<ha-formfield
.label=${html`<p>
${this.supervisor.localize("dialog.uninstall_addon.remove_data")}
${this.supervisor.localize("dialog.uninstall_app.remove_data")}
</p>`}
>
<ha-switch
@@ -1223,7 +1220,7 @@ class HassioAddonInfo extends LitElement {
></ha-switch>
</ha-formfield>
`,
confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"),
confirmText: this.supervisor.localize("dialog.uninstall_app.uninstall"),
dismissText: this.supervisor.localize("common.cancel"),
destructive: true,
});
@@ -1245,9 +1242,7 @@ class HassioAddonInfo extends LitElement {
button.actionSuccess();
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize(
"addon.dashboard.action_error.uninstall"
),
title: this.supervisor.localize("app.dashboard.action_error.uninstall"),
text: extractApiErrorMessage(err),
});
button.actionError();

View File

@@ -19,14 +19,14 @@ class HassioAddonSystemManaged extends LitElement {
return html`
<ha-alert
alert-type="warning"
.title=${this.supervisor.localize("addon.system_managed.title")}
.title=${this.supervisor.localize("app.system_managed.title")}
.narrow=${this.narrow}
>
${this.supervisor.localize("addon.system_managed.description")}
${this.supervisor.localize("app.system_managed.description")}
${!this.hideButton
? html`
<ha-button slot="action" @click=${this._takeControl}>
${this.supervisor.localize("addon.system_managed.take_control")}
${this.supervisor.localize("app.system_managed.take_control")}
</ha-button>
`
: nothing}

View File

@@ -216,7 +216,7 @@ export class SupervisorBackupContent extends LitElement {
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this.supervisor?.localize("backup.addons")}
.label=${this.supervisor?.localize("backup.apps")}
.iconPath=${mdiPuzzle}
>
</supervisor-formfield-label>`}

View File

@@ -33,13 +33,13 @@ class HassioAddons extends LitElement {
suffix
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this.supervisor.localize("dashboard.search_addons")}
.label=${this.supervisor.localize("dashboard.search_apps")}
>
</search-input>
</div>
<div class="content">
${!atLeastVersion(this.hass.config.version, 2021, 12)
? html`<h1>${this.supervisor.localize("dashboard.addons")}</h1>`
? html`<h1>${this.supervisor.localize("dashboard.apps")}</h1>`
: ""}
<div class="card-group">
${!this.supervisor.addon.addons.length
@@ -47,7 +47,7 @@ class HassioAddons extends LitElement {
<ha-card outlined>
<div class="card-content">
<button class="link" @click=${this._openStore}>
${this.supervisor.localize("dashboard.no_addons")}
${this.supervisor.localize("dashboard.no_apps")}
</button>
</div>
</ha-card>
@@ -67,14 +67,12 @@ class HassioAddons extends LitElement {
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? this.supervisor.localize("dashboard.addon_stopped")
? this.supervisor.localize("dashboard.app_stopped")
: addon.update_available!
? this.supervisor.localize(
"dashboard.addon_new_version"
"dashboard.app_new_version"
)
: this.supervisor.localize(
"dashboard.addon_running"
)}
: this.supervisor.localize("dashboard.app_running")}
.iconClass=${addon.update_available
? addon.state === "started"
? "update"

View File

@@ -39,7 +39,7 @@ class HassioDashboard extends LitElement {
.narrow=${this.narrow}
.route=${this.route}
back-path="/config"
.header=${this.supervisor.localize("panel.addons")}
.header=${this.supervisor.localize("panel.apps")}
>
<ha-icon-button
slot="toolbar-icon"
@@ -81,7 +81,7 @@ class HassioDashboard extends LitElement {
<span slot="header">
${this.supervisor.localize(
atLeastVersion(this.hass.config.version, 2021, 12)
? "panel.addons"
? "panel.apps"
: "panel.dashboard"
)}
</span>

View File

@@ -64,9 +64,9 @@ class HassioRepositoriesDialog extends LitElement {
repos
.filter(
(repo) =>
repo.slug !== "core" && // The core add-ons repository
repo.slug !== "local" && // Locally managed add-ons
repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons
repo.slug !== "core" && // The core apps repository
repo.slug !== "local" && // Locally managed apps
repo.slug !== "a0d7b954" && // Home Assistant Community Apps
repo.slug !== "5c53de3b" && // The ESPHome repository
repo.slug !== "d5369777" // Music Assistant repository
)

View File

@@ -16,11 +16,11 @@ export const suggestAddonRestart = async (
addon: HassioAddonDetails
): Promise<void> => {
const confirmed = await showConfirmationDialog(element, {
title: supervisor.localize("dialog.restart_addon.title", {
title: supervisor.localize("dialog.restart_app.title", {
name: addon.name,
}),
text: supervisor.localize("dialog.restart_addon.text"),
confirmText: supervisor.localize("dialog.restart_addon.restart"),
text: supervisor.localize("dialog.restart_app.text"),
confirmText: supervisor.localize("dialog.restart_app.restart"),
dismissText: supervisor.localize("common.cancel"),
});
if (confirmed) {

View File

@@ -89,14 +89,12 @@ class HassioSystemManagedDialog extends LitElement {
? html`<img src=${addonImage} alt=${this._addon.name} />`
: html`<ha-svg-icon .path=${mdiPuzzle}></ha-svg-icon>`}
</div>
${this._supervisor.localize("addon.system_managed.title")}.<br />
${this._supervisor.localize("addon.system_managed.description")}
${this._supervisor.localize("app.system_managed.title")}.<br />
${this._supervisor.localize("app.system_managed.description")}
${this._configEntry
? html`
<h3>
${this._supervisor.localize(
"addon.system_managed.managed_by"
)}:
${this._supervisor.localize("app.system_managed.managed_by")}:
</h3>
<ha-md-list>
<ha-md-list-item

View File

@@ -14,7 +14,7 @@ export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] =>
: [
{
translationKey: atLeastVersion(hass.config.version, 2021, 12)
? "panel.addons"
? "panel.apps"
: "panel.dashboard",
path: `/hassio/dashboard`,
iconPath: atLeastVersion(hass.config.version, 2021, 12)

View File

@@ -114,14 +114,14 @@ class HassioIngressView extends LitElement {
}
if (!addonInfo.version) {
await showAlertDialog(this, {
text: this.supervisor.localize("my.error_addon_not_installed"),
text: this.supervisor.localize("my.error_app_not_installed"),
title: addonInfo.name,
});
await nextRender();
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
} else if (!addonInfo.ingress) {
await showAlertDialog(this, {
text: this.supervisor.localize("my.error_addon_no_ingress"),
text: this.supervisor.localize("my.error_app_no_ingress"),
title: addonInfo.name,
});
await nextRender();
@@ -162,8 +162,8 @@ class HassioIngressView extends LitElement {
await this.updateComplete;
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_addon_info") ||
"Unable to fetch add-on info to start Ingress",
this.supervisor.localize("ingress.error_app_info") ||
"Unable to fetch app info to start Ingress",
title: "Supervisor",
});
await nextRender();
@@ -175,8 +175,8 @@ class HassioIngressView extends LitElement {
await this.updateComplete;
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_addon_not_installed") ||
"The add-on is not installed. Please install it first",
this.supervisor.localize("ingress.error_app_not_installed") ||
"The app is not installed. Please install it first",
title: addon.name,
});
await nextRender();
@@ -188,8 +188,8 @@ class HassioIngressView extends LitElement {
await this.updateComplete;
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_addon_not_supported") ||
"This add-on does not support Ingress",
this.supervisor.localize("ingress.error_app_not_supported") ||
"This app does not support Ingress",
title: addon.name,
});
await nextRender();
@@ -201,18 +201,18 @@ class HassioIngressView extends LitElement {
await this.updateComplete;
const confirm = await showConfirmationDialog(this, {
text:
this.supervisor.localize("ingress.error_addon_not_running") ||
"The add-on is not running. Do you want to start it now?",
this.supervisor.localize("ingress.error_app_not_running") ||
"The app is not running. Do you want to start it now?",
title: addon.name,
confirmText:
this.supervisor.localize("ingress.start_addon") || "Start add-on",
this.supervisor.localize("ingress.start_app") || "Start app",
dismissText: this.supervisor.localize("common.no") || "No",
});
if (confirm) {
try {
this._loadingMessage =
this.supervisor.localize("ingress.addon_starting") ||
"The add-on is starting, this can take some time...";
this.supervisor.localize("ingress.app_starting") ||
"The app is starting, this can take some time...";
await startHassioAddon(this.hass, addonSlug);
fireEvent(this, "supervisor-collection-refresh", {
collection: "addon",
@@ -222,8 +222,8 @@ class HassioIngressView extends LitElement {
} catch (_err) {
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_starting_addon") ||
"Error starting the add-on",
this.supervisor.localize("ingress.error_starting_app") ||
"Error starting the app",
title: addon.name,
});
await nextRender();
@@ -238,10 +238,10 @@ class HassioIngressView extends LitElement {
}
if (addon.state === "startup") {
// Addon is starting up, wait for it to start
// App is starting up, wait for it to start
this._loadingMessage =
this.supervisor.localize("ingress.addon_starting") ||
"The add-on is starting, this can take some time...";
this.supervisor.localize("ingress.app_starting") ||
"The app is starting, this can take some time...";
this._fetchDataTimeout = window.setTimeout(() => {
this._fetchData(addonSlug);
@@ -301,8 +301,8 @@ class HassioIngressView extends LitElement {
await this.updateComplete;
showConfirmationDialog(this, {
text:
this.supervisor.localize("ingress.error_addon_not_ready") ||
"The add-on seems to not be ready, it might still be starting. Do you want to try again?",
this.supervisor.localize("ingress.error_app_not_ready") ||
"The app seems to not be ready, it might still be starting. Do you want to try again?",
title: this._addon.name,
confirmText: this.supervisor.localize("ingress.retry") || "Retry",
dismissText: this.supervisor.localize("common.no") || "No",

View File

@@ -261,16 +261,16 @@ class UpdateAvailableCard extends LitElement {
private _computeCreateBackupTexts():
| { title: string; description?: string }
| undefined {
// Addon backup
// App backup
if (
this._updateType === "addon" &&
atLeastVersion(this.hass.config.version, 2025, 2, 0)
) {
const version = this._version;
return {
title: this.supervisor.localize("update_available.create_backup.addon"),
title: this.supervisor.localize("update_available.create_backup.app"),
description: this.supervisor.localize(
"update_available.create_backup.addon_description",
"update_available.create_backup.app_description",
{ version: version }
),
};
@@ -363,11 +363,11 @@ class UpdateAvailableCard extends LitElement {
)
) {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_arch"
"app.dashboard.not_available_arch"
);
} else {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_version",
"app.dashboard.not_available_version",
{
core_version_installed: this.supervisor.core.version,
core_version_needed: addonStoreInfo.homeassistant,

View File

@@ -1,4 +1,3 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import memoizeOne from "memoize-one";
import { type CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";

View File

@@ -26,39 +26,39 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.28.4",
"@babel/runtime": "7.28.6",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.20.0",
"@codemirror/commands": "6.10.0",
"@codemirror/language": "6.11.3",
"@codemirror/commands": "6.10.1",
"@codemirror/language": "6.12.1",
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.38.8",
"@codemirror/search": "6.6.0",
"@codemirror/state": "6.5.4",
"@codemirror/view": "6.39.11",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.2",
"@formatjs/intl-displaynames": "6.8.13",
"@formatjs/intl-durationformat": "0.7.6",
"@formatjs/intl-getcanonicallocales": "2.5.6",
"@formatjs/intl-listformat": "7.7.13",
"@formatjs/intl-locale": "4.2.13",
"@formatjs/intl-numberformat": "8.15.6",
"@formatjs/intl-pluralrules": "5.4.6",
"@formatjs/intl-relativetimeformat": "11.4.13",
"@fullcalendar/core": "6.1.19",
"@fullcalendar/daygrid": "6.1.19",
"@fullcalendar/interaction": "6.1.19",
"@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@formatjs/intl-datetimeformat": "7.2.0",
"@formatjs/intl-displaynames": "7.2.0",
"@formatjs/intl-durationformat": "0.10.0",
"@formatjs/intl-getcanonicallocales": "3.2.0",
"@formatjs/intl-listformat": "8.2.0",
"@formatjs/intl-locale": "5.2.0",
"@formatjs/intl-numberformat": "9.2.0",
"@formatjs/intl-pluralrules": "6.2.0",
"@formatjs/intl-relativetimeformat": "12.2.0",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
"@fullcalendar/interaction": "6.1.20",
"@fullcalendar/list": "6.1.20",
"@fullcalendar/luxon3": "6.1.20",
"@fullcalendar/timegrid": "6.1.20",
"@home-assistant/webawesome": "3.0.0-ha.2",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6",
"@lit-labs/motion": "1.1.0",
"@lit-labs/observers": "2.1.0",
"@lit-labs/virtualizer": "2.1.1",
"@lit/context": "1.1.6",
"@lit/reactive-element": "2.1.1",
"@lit/reactive-element": "2.1.2",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-base": "0.27.0",
@@ -85,12 +85,10 @@
"@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",
"@vaadin/combo-box": "24.9.6",
"@vaadin/vaadin-themable-mixin": "24.9.6",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
@@ -114,13 +112,13 @@
"hls.js": "1.6.15",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "10.7.18",
"intl-messageformat": "11.1.0",
"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.1",
"lit-html": "3.3.1",
"lit": "3.3.2",
"lit-html": "3.3.2",
"luxon": "3.7.2",
"marked": "17.0.1",
"memoize-one": "6.0.0",
@@ -135,7 +133,7 @@
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
"ua-parser-js": "2.0.6",
"ua-parser-js": "2.0.8",
"vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
@@ -148,20 +146,20 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.28.5",
"@babel/core": "7.28.6",
"@babel/helper-define-polyfill-provider": "0.6.5",
"@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.5",
"@bundle-stats/plugin-webpack-filter": "4.21.7",
"@lokalise/node-api": "15.4.0",
"@babel/preset-env": "7.28.6",
"@bundle-stats/plugin-webpack-filter": "4.21.8",
"@lokalise/node-api": "15.6.0",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.3.12",
"@rspack/core": "1.6.6",
"@rspack/dev-server": "1.1.4",
"@rsdoctor/rspack-plugin": "1.5.0",
"@rspack/core": "1.7.2",
"@rspack/dev-server": "1.1.5",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.22",
"@types/chromecast-caf-receiver": "6.0.25",
"@types/chromecast-caf-sender": "1.0.11",
"@types/color-name": "2.0.0",
"@types/culori": "4.0.1",
@@ -178,12 +176,12 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.15",
"@vitest/coverage-v8": "4.0.17",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.1",
"eslint": "9.39.1",
"eslint": "9.39.2",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.10",
@@ -193,7 +191,7 @@
"eslint-plugin-unused-imports": "4.3.0",
"eslint-plugin-wc": "3.0.2",
"fancy-log": "2.0.0",
"fs-extra": "11.3.2",
"fs-extra": "11.3.3",
"glob": "13.0.0",
"gulp": "5.0.1",
"gulp-brotli": "3.0.0",
@@ -201,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",
@@ -209,35 +207,35 @@
"lodash.template": "4.5.0",
"map-stream": "0.0.7",
"pinst": "3.0.0",
"prettier": "3.7.4",
"rspack-manifest-plugin": "5.2.0",
"prettier": "3.8.0",
"rspack-manifest-plugin": "5.2.1",
"serve": "14.2.5",
"sinon": "21.0.0",
"tar": "7.5.2",
"terser-webpack-plugin": "5.3.15",
"sinon": "21.0.1",
"tar": "7.5.3",
"terser-webpack-plugin": "5.3.16",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.48.1",
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.15",
"typescript-eslint": "8.53.0",
"vite-tsconfig-paths": "6.0.4",
"vitest": "4.0.17",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
},
"resolutions": {
"@material/mwc-button@^0.25.3": "^0.27.0",
"lit": "3.3.1",
"lit-html": "3.3.1",
"lit": "3.3.2",
"lit-html": "3.3.2",
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.1",
"@fullcalendar/daygrid": "6.1.19",
"globals": "16.5.0",
"@lit/reactive-element": "2.1.2",
"@fullcalendar/daygrid": "6.1.20",
"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"
},
"packageManager": "yarn@4.12.0",
"volta": {
"node": "22.21.1"
"node": "24.13.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

@@ -1,5 +1,4 @@
#!/bin/sh
# Pushes a new version to PyPi.
# Stop on errors
set -e
@@ -12,5 +11,4 @@ yarn install
script/build_frontend
rm -rf dist home_assistant_frontend.egg-info
python3 -m build
python3 -m twine upload dist/*.whl --skip-existing
python3 -m build -q

View File

@@ -38,13 +38,11 @@ export class HaAuthFormString extends HaFormString {
}
</style>
<ha-auth-textfield
.type=${
!this.isPassword
.type=${!this.isPassword
? this.stringType
: this.unmaskedPassword
? "text"
: "password"
}
: "password"}
.label=${this.label}
.value=${this.data || ""}
.helper=${this.helper}
@@ -55,18 +53,17 @@ export class HaAuthFormString extends HaFormString {
.name=${this.schema.name}
.autocomplete=${this.schema.autocomplete}
?autofocus=${this.schema.autofocus}
.suffix=${
this.isPassword
? // reserve some space for the icon.
html`<div style="width: 24px"></div>`
: this.schema.description?.suffix
}
.validationMessage=${this.schema.required ? this.localize?.("ui.panel.page-authorize.form.error_required") : undefined}
.suffix=${this.isPassword
? // reserve some space for the icon.
html`<div style="width: 24px"></div>`
: this.schema.description?.suffix}
.validationMessage=${this.schema.required
? this.localize?.("ui.panel.page-authorize.form.error_required")
: undefined}
@input=${this._valueChanged}
@change=${this._valueChanged}
></ha-auth-textfield>
${this.renderIcon()}
</ha-auth-textfield>
></ha-auth-textfield>
${this.renderIcon()}
`;
}
}

View File

@@ -1,4 +1,4 @@
import type { AreaRegistryEntry } from "../../data/area_registry";
import type { AreaRegistryEntry } from "../../data/area/area_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
export interface AreasFloorHierarchy {

View File

@@ -1,7 +1,6 @@
export const THEME_COLORS = new Set([
"primary",
"accent",
"disabled",
"red",
"pink",
"purple",
@@ -25,6 +24,9 @@ export const THEME_COLORS = new Set([
"blue-grey",
"black",
"white",
"primary-text",
"secondary-text",
"disabled",
]);
export function computeCssColor(color: string): string {

View File

@@ -79,7 +79,7 @@ export const generateColorPalette = (
}
return steps.map((step) => {
const name = `color-${label}-${step}`;
const name = `ha-color-${label}-${step}`;
// Base color at 50%
if (step === 50) {

View File

@@ -93,8 +93,8 @@ export const calcDateRange = (
];
case "now-12m":
return [
calcDate(subMonths(today, 12), startOfMonth, hass.locale, hass.config),
calcDate(subMonths(today, 1), endOfMonth, hass.locale, hass.config),
calcDate(today, subMonths, hass.locale, hass.config, 12),
calcDate(today, subMonths, hass.locale, hass.config, 0),
];
case "now-1h":
return [

View File

@@ -1,4 +1,4 @@
import type { AreaRegistryEntry } from "../../data/area_registry";
import type { AreaRegistryEntry } from "../../data/area/area_registry";
export const computeAreaName = (area: AreaRegistryEntry): string | undefined =>
area.name?.trim();

View File

@@ -1,4 +1,4 @@
import type { AreaRegistryEntry } from "../../../data/area_registry";
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import type { FloorRegistryEntry } from "../../../data/floor_registry";
import type { HomeAssistant } from "../../../types";

View File

@@ -1,4 +1,4 @@
import type { AreaRegistryEntry } from "../../../data/area_registry";
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import type { FloorRegistryEntry } from "../../../data/floor_registry";
import type { HomeAssistant } from "../../../types";

View File

@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import type {
EntityRegistryDisplayEntry,

View File

@@ -15,6 +15,7 @@ export interface EntityFilter {
label?: string | string[];
entity_category?: EntityCategory | EntityCategory[];
hidden_platform?: string | string[];
hidden_domains?: string | string[];
}
export type EntityFilterFunc = (entityId: string) => boolean;
@@ -38,6 +39,9 @@ export const generateEntityFilter = (
const domains = filter.domain
? new Set(ensureArray(filter.domain))
: undefined;
const hiddenDomains = filter.hidden_domains
? new Set(ensureArray(filter.hidden_domains))
: undefined;
const deviceClasses = filter.device_class
? new Set(ensureArray(filter.device_class))
: undefined;
@@ -57,12 +61,16 @@ export const generateEntityFilter = (
if (!stateObj) {
return false;
}
if (domains) {
if (domains || hiddenDomains) {
const domain = computeDomain(entityId);
if (!domains.has(domain)) {
if (domains && !domains.has(domain)) {
return false;
}
if (hiddenDomains && hiddenDomains.has(domain)) {
return false;
}
}
if (deviceClasses) {
const dc = stateObj.attributes.device_class || "none";
if (!deviceClasses.has(dc)) {

View File

@@ -0,0 +1,53 @@
/**
* ES5-compatible implementation of the keyed directive.
* Based on lit-html's keyed directive but written to avoid ES5 minification issues.
*
* This implementation avoids parameter destructuring in the update() method,
* which causes Terser with ecma: 5 to generate invalid references like `_k`.
*
* Used only for ES5 builds (legacy browsers). Modern builds use the original
* lit-html keyed directive.
*
* @see https://github.com/home-assistant/frontend/issues/28732
*/
// eslint-disable-next-line import/extensions
import { directive, Directive } from "lit-html/directive.js";
// eslint-disable-next-line import/extensions
import { setCommittedValue } from "lit-html/directive-helpers.js";
// eslint-disable-next-line lit/no-legacy-imports
import { nothing } from "lit-html";
// eslint-disable-next-line import/extensions
import type { Part } from "lit-html/directive.js";
class KeyedES5 extends Directive {
private _key: unknown = nothing;
render(k: unknown, v: unknown) {
this._key = k;
return v;
}
update(part: unknown, args: [unknown, unknown]) {
const k = args[0];
const v = args[1];
if (k !== this._key) {
// Clear the part before returning a value. The one-arg form of
// setCommittedValue sets the value to a sentinel which forces a
// commit the next render.
setCommittedValue(part as Part);
this._key = k;
}
return v;
}
}
/**
* Associates a renderable value with a unique key. When the key changes, the
* previous DOM is removed and disposed before rendering the next value, even
* if the value - such as a template - is the same.
*
* This is useful for forcing re-renders of stateful components, or working
* with code that expects new data to generate new HTML elements, such as some
* animation techniques.
*/
export const keyed = directive(KeyedES5);

View File

@@ -17,32 +17,45 @@ export interface NavigateOptions {
// max time to wait for dialogs to close before navigating
const DIALOG_WAIT_TIMEOUT = 500;
export const navigate = async (
path: string,
options?: NavigateOptions,
timestamp = Date.now()
) => {
/**
* Ensures all dialogs are closed before navigation.
* Returns true if navigation can proceed, false if a dialog refused to close.
*/
const ensureDialogsClosed = async (timestamp: number): Promise<boolean> => {
const { history } = mainWindow;
if (history.state?.dialog && Date.now() - timestamp < DIALOG_WAIT_TIMEOUT) {
const closed = await closeAllDialogs();
if (!closed) {
// eslint-disable-next-line no-console
console.warn("Navigation blocked, because dialog refused to close");
return false;
}
return new Promise<boolean>((resolve) => {
// need to wait for history state to be updated in case a dialog was closed
setTimeout(() => {
navigate(path, options, timestamp).then(resolve);
});
});
if (!history.state?.dialog || Date.now() - timestamp >= DIALOG_WAIT_TIMEOUT) {
return true;
}
const closed = await closeAllDialogs();
if (!closed) {
// eslint-disable-next-line no-console
console.warn("Navigation blocked, because dialog refused to close");
return false;
}
// wait for history state to be updated after dialog closed
await new Promise<void>((resolve) => {
setTimeout(resolve);
});
return ensureDialogsClosed(timestamp);
};
export const navigate = async (path: string, options?: NavigateOptions) => {
const canProceed = await ensureDialogsClosed(Date.now());
if (!canProceed) {
return false;
}
const replace = options?.replace || false;
if (__DEMO__) {
if (replace) {
history.replaceState(
history.state?.root ? { root: true } : (options?.data ?? null),
mainWindow.history.replaceState(
mainWindow.history.state?.root
? { root: true }
: (options?.data ?? null),
"",
`${mainWindow.location.pathname}#${path}`
);
@@ -50,13 +63,13 @@ export const navigate = async (
mainWindow.location.hash = path;
}
} else if (replace) {
history.replaceState(
history.state?.root ? { root: true } : (options?.data ?? null),
mainWindow.history.replaceState(
mainWindow.history.state?.root ? { root: true } : (options?.data ?? null),
"",
path
);
} else {
history.pushState(options?.data ?? null, "", path);
mainWindow.history.pushState(options?.data ?? null, "", path);
}
fireEvent(mainWindow, "location-changed", {
replace,
@@ -68,10 +81,14 @@ export const navigate = async (
* Navigate back in history, with fallback to a default path if no history exists.
* This prevents a user from getting stuck when they navigate directly to a page with no history.
*/
export const goBack = (fallbackPath?: string) => {
const { history } = mainWindow;
export const goBack = async (fallbackPath?: string): Promise<void> => {
const canProceed = await ensureDialogsClosed(Date.now());
if (!canProceed) {
return;
}
// Check if we have history to go back to
const { history } = mainWindow;
if (history.length > 1) {
history.back();
return;

14
src/common/url/route.ts Normal file
View File

@@ -0,0 +1,14 @@
import type { Route } from "../../types";
export const computeRouteTail = (route: Route) => {
const dividerPos = route.path.indexOf("/", 1);
return dividerPos === -1
? {
prefix: route.prefix + route.path,
path: "",
}
: {
prefix: route.prefix + route.path.substring(0, dividerPos),
path: route.path.substring(dividerPos),
};
};

View File

@@ -1,6 +1,16 @@
// From https://github.com/epoberezkin/fast-deep-equal
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
export const deepEqual = (a: any, b: any): boolean => {
interface DeepEqualOptions {
/** Compare Symbol properties in addition to string keys */
compareSymbols?: boolean;
}
export const deepEqual = (
a: any,
b: any,
options?: DeepEqualOptions
): boolean => {
if (a === b) {
return true;
}
@@ -18,7 +28,7 @@ export const deepEqual = (a: any, b: any): boolean => {
return false;
}
for (i = length; i-- !== 0; ) {
if (!deepEqual(a[i], b[i])) {
if (!deepEqual(a[i], b[i], options)) {
return false;
}
}
@@ -35,7 +45,7 @@ export const deepEqual = (a: any, b: any): boolean => {
}
}
for (i of a.entries()) {
if (!deepEqual(i[1], b.get(i[0]))) {
if (!deepEqual(i[1], b.get(i[0]), options)) {
return false;
}
}
@@ -93,11 +103,28 @@ export const deepEqual = (a: any, b: any): boolean => {
for (i = length; i-- !== 0; ) {
const key = keys[i];
if (!deepEqual(a[key], b[key])) {
if (!deepEqual(a[key], b[key], options)) {
return false;
}
}
// Compare Symbol properties if requested
if (options?.compareSymbols) {
const symbolsA = Object.getOwnPropertySymbols(a);
const symbolsB = Object.getOwnPropertySymbols(b);
if (symbolsA.length !== symbolsB.length) {
return false;
}
for (const sym of symbolsA) {
if (!Object.prototype.hasOwnProperty.call(b, sym)) {
return false;
}
if (!deepEqual(a[sym], b[sym], options)) {
return false;
}
}
}
return true;
}

View File

@@ -0,0 +1,19 @@
export const startMediaProgressInterval = (
interval: number | undefined,
callback: () => void,
intervalMs = 1000
): number => {
if (interval) {
return interval;
}
return window.setInterval(callback, intervalMs);
};
export const stopMediaProgressInterval = (
interval: number | undefined
): number | undefined => {
if (interval) {
clearInterval(interval);
}
return undefined;
};

View File

@@ -1,30 +1,45 @@
/**
* Executes a callback within a View Transition if supported, otherwise runs it directly.
* Executes a synchronous callback within a View Transition if supported, otherwise runs it directly.
*
* @param callback - Function to execute. Can be synchronous or return a Promise. The callback will be passed a boolean indicating whether the view transition is available.
* @param callback - Synchronous function to execute. The callback will be passed a boolean indicating whether the view transition is available.
* @returns Promise that resolves when the transition completes (or immediately if not supported)
*
* @example
* ```typescript
* // Synchronous callback
* withViewTransition(() => {
* this.large = !this.large;
* });
*
* // Async callback
* await withViewTransition(async () => {
* await this.updateData();
* });
* ```
*/
export const withViewTransition = (
callback: (viewTransitionAvailable: boolean) => void | Promise<void>
callback: (viewTransitionAvailable: boolean) => void
): Promise<void> => {
if (document.startViewTransition) {
return document.startViewTransition(() => callback(true)).finished;
if (!document.startViewTransition) {
callback(false);
return Promise.resolve();
}
// Fallback: Execute callback directly without transition
const result = callback(false);
return result instanceof Promise ? result : Promise.resolve();
let callbackInvoked = false;
try {
// View Transitions require DOM updates to happen synchronously within
// the callback. Execute the callback immediately (synchronously).
const transition = document.startViewTransition(() => {
callbackInvoked = true;
callback(true);
});
return transition.finished;
} catch (err) {
// eslint-disable-next-line no-console
console.warn(
"View transition failed, falling back to direct execution.",
err
);
// Make sure the callback is invoked exactly once.
if (!callbackInvoked) {
callback(false);
return Promise.resolve();
}
return Promise.reject(err);
}
};

View File

@@ -0,0 +1,186 @@
import type { HaSlider } from "../../components/ha-slider";
interface VolumeSliderControllerOptions {
getSlider: () => HaSlider | undefined;
step: number;
onSetVolume: (value: number) => void;
onSetVolumeDebounced?: (value: number) => void;
onValueUpdated?: (value: number) => void;
}
export class VolumeSliderController {
private _touchStartX = 0;
private _touchStartY = 0;
private _touchStartValue = 0;
private _touchDragging = false;
private _touchScrolling = false;
private _dragging = false;
private _lastValue = 0;
private _options: VolumeSliderControllerOptions;
constructor(options: VolumeSliderControllerOptions) {
this._options = options;
}
public get isInteracting(): boolean {
return this._touchDragging || this._dragging;
}
public setStep(step: number): void {
this._options.step = step;
}
public handleInput = (ev: Event): void => {
ev.stopPropagation();
const value = Number((ev.target as HaSlider).value);
this._dragging = true;
this._updateValue(value);
this._options.onSetVolumeDebounced?.(value);
};
public handleChange = (ev: Event): void => {
ev.stopPropagation();
const value = Number((ev.target as HaSlider).value);
this._dragging = false;
this._updateValue(value);
this._options.onSetVolume(value);
};
public handleTouchStart = (ev: TouchEvent): void => {
ev.stopPropagation();
const touch = ev.touches[0];
this._touchStartX = touch.clientX;
this._touchStartY = touch.clientY;
this._touchStartValue = this._getSliderValue();
this._touchDragging = false;
this._touchScrolling = false;
this._showTooltip();
};
public handleTouchMove = (ev: TouchEvent): void => {
if (this._touchScrolling) {
return;
}
const touch = ev.touches[0];
const deltaX = touch.clientX - this._touchStartX;
const deltaY = touch.clientY - this._touchStartY;
const absDeltaX = Math.abs(deltaX);
const absDeltaY = Math.abs(deltaY);
if (!this._touchDragging) {
if (absDeltaY > 10 && absDeltaY > absDeltaX * 2) {
this._touchScrolling = true;
return;
}
if (absDeltaX > 8) {
this._touchDragging = true;
}
}
if (this._touchDragging) {
ev.preventDefault();
const newValue = this._getVolumeFromTouch(touch.clientX);
this._updateValue(newValue);
}
};
public handleTouchEnd = (ev: TouchEvent): void => {
if (this._touchScrolling) {
this._touchScrolling = false;
this._hideTooltip();
return;
}
const touch = ev.changedTouches[0];
if (!this._touchDragging) {
const tapValue = this._getVolumeFromTouch(touch.clientX);
const delta =
tapValue > this._touchStartValue
? this._options.step
: -this._options.step;
const newValue = this._roundVolumeValue(this._touchStartValue + delta);
this._updateValue(newValue);
this._options.onSetVolume(newValue);
} else {
const finalValue = this._getVolumeFromTouch(touch.clientX);
this._updateValue(finalValue);
this._options.onSetVolume(finalValue);
}
this._touchDragging = false;
this._dragging = false;
this._hideTooltip();
};
public handleTouchCancel = (): void => {
this._touchDragging = false;
this._touchScrolling = false;
this._dragging = false;
this._updateValue(this._touchStartValue);
this._hideTooltip();
};
public handleWheel = (ev: WheelEvent): void => {
ev.preventDefault();
ev.stopPropagation();
const direction = ev.deltaY > 0 ? -1 : 1;
const currentValue = this._getSliderValue();
const newValue = this._roundVolumeValue(
currentValue + direction * this._options.step
);
this._updateValue(newValue);
this._options.onSetVolume(newValue);
};
private _getVolumeFromTouch(clientX: number): number {
const slider = this._options.getSlider();
if (!slider) {
return 0;
}
const rect = slider.getBoundingClientRect();
const x = Math.min(Math.max(clientX - rect.left, 0), rect.width);
const percentage = (x / rect.width) * 100;
return this._roundVolumeValue(percentage);
}
private _roundVolumeValue(value: number): number {
return Math.min(
Math.max(Math.round(value / this._options.step) * this._options.step, 0),
100
);
}
private _getSliderValue(): number {
const slider = this._options.getSlider();
if (slider) {
return Number(slider.value);
}
return this._lastValue;
}
private _updateValue(value: number): void {
this._lastValue = value;
this._options.onValueUpdated?.(value);
const slider = this._options.getSlider();
if (slider) {
slider.value = value;
}
}
private _showTooltip(): void {
const slider = this._options.getSlider() as any;
slider?.showTooltip?.();
}
private _hideTooltip(): void {
const slider = this._options.getSlider() as any;
slider?.hideTooltip?.();
}
}

View File

@@ -2,9 +2,13 @@ import { customElement, property, state } from "lit/decorators";
import { LitElement, html, css } from "lit";
import type { EChartsType } from "echarts/core";
import type { SankeySeriesOption } from "echarts/types/dist/echarts";
import type { CallbackDataParams } from "echarts/types/src/util/types";
import type {
CallbackDataParams,
ECElementEvent,
} from "echarts/types/src/util/types";
import memoizeOne from "memoize-one";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { fireEvent } from "../../common/dom/fire_event";
import SankeyChart from "../../resources/echarts/components/sankey/install";
import type { HomeAssistant } from "../../types";
import type { ECOption } from "../../resources/echarts/echarts";
@@ -21,6 +25,7 @@ export interface Node {
label?: string;
color?: string;
passThrough?: boolean;
entityId?: string;
}
export interface Link {
source: string;
@@ -83,6 +88,7 @@ export class HaSankeyChart extends LitElement {
.options=${options}
height="100%"
.extraComponents=${[SankeyChart]}
@chart-click=${this._handleChartClick}
></ha-chart-base>`;
}
@@ -103,6 +109,22 @@ export class HaSankeyChart extends LitElement {
return null;
};
private _handleChartClick = (ev: CustomEvent<ECElementEvent>) => {
const detail = ev.detail;
// Only handle node clicks (not links)
if (detail.dataType !== "node") {
return;
}
const nodeId = (detail.data as Record<string, any>)?.id;
if (!nodeId) {
return;
}
const node = this.data.nodes.find((n) => n.id === nodeId);
if (node?.entityId) {
fireEvent(this, "node-click", { node });
}
};
private _createData = memoizeOne((data: SankeyChartData, width = 0) => {
const filteredNodes = data.nodes.filter((n) => n.value > 0);
const indexes = [...new Set(filteredNodes.map((n) => n.index))].sort();
@@ -294,4 +316,7 @@ declare global {
interface HTMLElementTagNameMap {
"ha-sankey-chart": HaSankeyChart;
}
interface HASSDomEvents {
"node-click": { node: Node };
}
}

View File

@@ -0,0 +1,207 @@
import type { EChartsType } from "echarts/core";
import type { SunburstSeriesOption } from "echarts/types/dist/echarts";
import type { CallbackDataParams } from "echarts/types/src/util/types";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { getGraphColorByIndex } from "../../common/color/colors";
import { filterXSS } from "../../common/util/xss";
import type { ECOption } from "../../resources/echarts/echarts";
import type { HomeAssistant } from "../../types";
import "./ha-chart-base";
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
let SunburstChart: typeof import("echarts/lib/chart/sunburst/install");
export interface SunburstNode {
id: string;
name?: string;
value: number;
itemStyle?: {
color?: string;
};
children?: SunburstNode[];
}
@customElement("ha-sunburst-chart")
export class HaSunburstChart extends LitElement {
public hass!: HomeAssistant;
@property({ attribute: false }) public data?: SunburstNode;
@property({ type: String, attribute: false }) public valueFormatter?: (
value: number
) => string;
public chart?: EChartsType;
constructor() {
super();
if (!SunburstChart) {
import("echarts/lib/chart/sunburst/install").then((module) => {
SunburstChart = module;
this.requestUpdate();
});
}
}
render() {
if (!SunburstChart || !this.data) {
return nothing;
}
const options = {
tooltip: {
trigger: "item",
formatter: this._renderTooltip,
appendTo: document.body,
},
} as ECOption;
return html`<ha-chart-base
.data=${this._createData(this.data)}
.options=${options}
height="100%"
.extraComponents=${[SunburstChart]}
></ha-chart-base>`;
}
private _renderTooltip = (params: CallbackDataParams) => {
const data = params.data as { name: string; value: number };
const value = this.valueFormatter
? this.valueFormatter(data.value)
: data.value;
return `${params.marker} ${filterXSS(data.name)}<br>${value}`;
};
private _createData = memoizeOne(
(data: SunburstNode): SunburstSeriesOption => {
const computedStyles = getComputedStyle(this);
// Transform to echarts format (uses 'name' instead of 'id')
const transformNode = (
node: SunburstNode,
index: number,
depth: number,
parentColor?: string
) => {
const result = {
...node,
name: node.name || node.id,
};
if (depth > 0 && !node.itemStyle?.color) {
// Don't assign color to root node
result.itemStyle = {
color: parentColor ?? getGraphColorByIndex(index, computedStyles),
};
}
if (node.children && node.children.length > 0) {
result.children = node.children.map((child, i) =>
transformNode(child, i, depth + 1, result.itemStyle?.color)
);
}
return result;
};
const transformedData = transformNode(data, 0, 0);
return {
type: "sunburst",
data: transformedData.children || [transformedData],
radius: [0, "90%"],
sort: undefined, // Keep original order
label: {
show: false,
align: "center",
rotate: "radial",
minAngle: 15,
hideOverlap: true,
},
emphasis: {
focus: "ancestor",
label: {
show: false,
},
},
itemStyle: {
borderRadius: 2,
},
levels: this._generateLevels(this._getMaxDepth(data)),
} as SunburstSeriesOption;
}
);
private _getMaxDepth(node: SunburstNode, currentDepth = 0): number {
if (!node.children || node.children.length === 0) {
return currentDepth;
}
return Math.max(
...node.children.map((child) =>
this._getMaxDepth(child, currentDepth + 1)
)
);
}
private _generateLevels(depth: number): SunburstSeriesOption["levels"] {
const levels: SunburstSeriesOption["levels"] = [];
// Root level (center) - transparent, small fixed size
const rootRadius = 15;
const outerRadius = 95;
const availableRadius = outerRadius - rootRadius;
levels.push({
r0: "0%",
r: `${rootRadius}%`,
itemStyle: {
color: "transparent",
},
});
if (depth === 0) {
return levels;
}
// Distribute remaining radius among data levels using weighted distribution
// First level gets most space, each subsequent level gets progressively smaller
const weights = Array.from({ length: depth }, (_, i) => depth - i);
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
let currentRadius = rootRadius;
for (let i = 0; i < depth; i++) {
const levelRadius = (weights[i] / totalWeight) * availableRadius;
const r0 = currentRadius;
const r = currentRadius + levelRadius;
currentRadius = r;
levels.push({
r0: `${r0}%`,
r: `${r}%`,
// Show labels only on first level
...(i === 0 ? { label: { show: true } } : {}),
});
}
return levels;
}
static styles = css`
:host {
display: block;
flex: 1;
}
ha-chart-base {
width: 100%;
height: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-sunburst-chart": HaSunburstChart;
}
}

View File

@@ -21,6 +21,7 @@ import { measureTextWidth } from "../../util/text";
import { fireEvent } from "../../common/dom/fire_event";
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
import { filterXSS } from "../../common/util/xss";
const safeParseFloat = (value) => {
const parsed = parseFloat(value);
@@ -184,7 +185,7 @@ export class StateHistoryChartLine extends LitElement {
}
if (param.seriesName) {
return `${param.marker} ${param.seriesName}: ${value}`;
return `${param.marker} ${filterXSS(param.seriesName)}: ${value}`;
}
return `${param.marker} ${value}`;
})

View File

@@ -184,17 +184,11 @@ export class StatisticsChart extends LitElement {
}
private _datasetHidden(ev: CustomEvent) {
if (!this._legendData) {
return;
}
this._hiddenStats.add(ev.detail.id);
this.requestUpdate("_hiddenStats");
}
private _datasetUnhidden(ev: CustomEvent) {
if (!this._legendData) {
return;
}
this._hiddenStats.delete(ev.detail.id);
this.requestUpdate("_hiddenStats");
}
@@ -521,7 +515,9 @@ export class StatisticsChart extends LitElement {
`ui.components.statistics_charts.statistic_types.${type}`
),
symbol: "none",
sampling: "minmax",
// minmax sampling operates independently per series, breaking stacking alignment
// https://github.com/apache/echarts/issues/11879
sampling: band && drawBands ? "lttb" : "minmax",
animationDurationUpdate: 0,
lineStyle: {
width: 1.5,
@@ -539,10 +535,17 @@ export class StatisticsChart extends LitElement {
if (band && this.chartType === "line") {
series.stack = `band-${statistic_id}`;
series.stackStrategy = "all";
if (drawBands && type === bandTop) {
(series as LineSeriesOption).areaStyle = {
color: color + "3F",
};
if (this._hiddenStats.has(`${statistic_id}-${bandBottom}`)) {
// changing the stackOrder forces echarts to render the stacked series that are not hidden #28472
series.stackOrder = "seriesDesc";
(series as LineSeriesOption).areaStyle = undefined;
} else {
series.stackOrder = "seriesAsc";
if (drawBands && type === bandTop) {
(series as LineSeriesOption).areaStyle = {
color: color + "3F",
};
}
}
}
if (!this.hideLegend) {
@@ -586,7 +589,8 @@ export class StatisticsChart extends LitElement {
} else if (
type === bandTop &&
this.chartType === "line" &&
drawBands
drawBands &&
!this._hiddenStats.has(`${statistic_id}-${bandBottom}`)
) {
const top = stat[bandTop] || 0;
val.push(Math.abs(top - (stat[bandBottom] || 0)));

View File

@@ -1364,6 +1364,9 @@ export class HaDataTable extends LitElement {
.mdc-data-table__header-cell > * {
transition: var(--float-start) 0.2s ease;
}
.mdc-data-table__header-cell--numeric > span {
transition: none;
}
.mdc-data-table__header-cell ha-svg-icon {
top: -3px;
position: absolute;
@@ -1402,6 +1405,9 @@ export class HaDataTable extends LitElement {
}
.secondary {
color: var(--secondary-text-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.scroller {
height: calc(100% - 57px);

View File

@@ -1,9 +1,9 @@
import { expose } from "comlink";
import Fuse, { type FuseOptionKey } from "fuse.js";
import type { FuseOptionKey, IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import memoizeOne from "memoize-one";
import { ipCompare, stringCompare } from "../../common/string/compare";
import { stripDiacritics } from "../../common/string/strip-diacritics";
import { multiTermSearch } from "../../resources/fuseMultiTerm";
import type {
ClonedDataTableColumnData,
DataTableRowData,
@@ -11,46 +11,159 @@ import type {
SortingDirection,
} from "./ha-data-table";
const getSearchKeys = memoizeOne(
(columns: SortableColumnContainer): FuseOptionKey<DataTableRowData>[] => {
const searchKeys = new Set<string>();
interface FilterKeyConfig {
key: string;
filterKey?: string;
}
Object.entries(columns).forEach(([key, column]) => {
if (column.filterable) {
searchKeys.add(
column.filterKey
? `${column.valueColumn || key}.${column.filterKey}`
: key
);
}
});
return Array.from(searchKeys);
const getFilterKeys = memoizeOne(
(columns: SortableColumnContainer): FilterKeyConfig[] =>
Object.entries(columns)
.filter(([, column]) => column.filterable)
.map(([key, column]) => ({
key: column.valueColumn || key,
filterKey: column.filterKey,
}))
);
const getSearchableValue = (
row: DataTableRowData,
{ key, filterKey }: FilterKeyConfig
): string => {
let value = row[key];
if (value == null) {
return "";
}
);
const fuseIndex = memoizeOne(
(data: DataTableRowData[], keys: FuseOptionKey<DataTableRowData>[]) =>
Fuse.createIndex(keys, data)
);
if (filterKey && typeof value === "object" && !Array.isArray(value)) {
value = value[filterKey];
if (value == null) {
return "";
}
}
if (Array.isArray(value)) {
const stringValues = value
.filter((item) => item != null && typeof item !== "object")
.map(String);
return stripDiacritics(stringValues.join(" ").toLowerCase());
}
return stripDiacritics(String(value).toLowerCase());
};
/** Filters data using exact substring matching (all terms must match). */
const filterDataExact = (
data: DataTableRowData[],
filterKeys: FilterKeyConfig[],
terms: string[]
): DataTableRowData[] => {
if (terms.length === 1) {
const term = terms[0];
return data.filter((row) =>
filterKeys.some((config) =>
getSearchableValue(row, config).includes(term)
)
);
}
return data.filter((row) => {
const searchString = filterKeys
.map((config) => getSearchableValue(row, config))
.join(" ");
return terms.every((term) => searchString.includes(term));
});
};
const FUZZY_OPTIONS: IFuseOptions<DataTableRowData> = {
ignoreDiacritics: true,
isCaseSensitive: false,
threshold: 0.2, // Stricter than default 0.3
minMatchCharLength: 2,
ignoreLocation: true,
shouldSort: false,
};
interface FuseKeyConfig {
name: string | string[];
getFn: (row: DataTableRowData) => string;
}
/** Filters data using fuzzy matching with Fuse.js (all terms must match). */
const filterDataFuzzy = (
data: DataTableRowData[],
filterKeys: FilterKeyConfig[],
terms: string[]
): DataTableRowData[] => {
// Build Fuse.js search keys from filter keys
const fuseKeys: FuseKeyConfig[] = filterKeys.map((config) => ({
name: config.filterKey ? [config.key, config.filterKey] : config.key,
getFn: (row: DataTableRowData) => getSearchableValue(row, config),
}));
// Find minimum term length to adjust minMatchCharLength
const minTermLength = Math.min(...terms.map((t) => t.length));
const minMatchCharLength = Math.min(minTermLength, 2);
const fuse = new Fuse<DataTableRowData>(data, {
...FUZZY_OPTIONS,
keys: fuseKeys as FuseOptionKey<DataTableRowData>[],
minMatchCharLength,
});
// For single term, simple search
if (terms.length === 1) {
return fuse.search(terms[0]).map((r) => r.item);
}
// For multiple terms, all must match (AND logic)
const expression = {
$and: terms.map((term) => ({
$or: fuseKeys.map((key) => ({
$path: Array.isArray(key.name) ? key.name : [key.name],
$val: term,
})),
})),
};
return fuse.search(expression).map((r) => r.item);
};
/**
* Filters data with exact match priority and fuzzy fallback.
* - First tries exact substring matching
* - If exact matches found, returns only those
* - If no exact matches, falls back to fuzzy search with strict scoring
*/
const filterData = (
data: DataTableRowData[],
columns: SortableColumnContainer,
filter: string
) => {
filter = stripDiacritics(filter.toLowerCase());
): DataTableRowData[] => {
const normalizedFilter = stripDiacritics(filter.toLowerCase().trim());
if (filter === "") {
if (!normalizedFilter) {
return data;
}
const keys = getSearchKeys(columns);
const filterKeys = getFilterKeys(columns);
const index = fuseIndex(data, keys);
if (!filterKeys.length) {
return data;
}
return multiTermSearch<DataTableRowData>(data, filter, keys, index, {
threshold: 0.2, // reduce fuzzy matches in data tables
});
const terms = normalizedFilter.split(/\s+/);
// First, try exact substring matching
const exactMatches = filterDataExact(data, filterKeys, terms);
if (exactMatches.length > 0) {
return exactMatches;
}
// No exact matches, fall back to fuzzy search
return filterDataFuzzy(data, filterKeys, terms);
};
const sortData = (

View File

@@ -101,6 +101,10 @@ const Component = Vue.extend({
type: String,
default: "en",
},
opensVertical: {
type: String,
default: undefined,
},
},
render(createElement) {
// @ts-expect-error
@@ -129,6 +133,11 @@ const Component = Vue.extend({
},
expression: "dateRange",
},
on: {
toggle: (open: boolean) => {
fireEvent(this.$el as HTMLElement, "toggle", { open });
},
},
scopedSlots: {
input() {
return createElement("slot", {
@@ -309,6 +318,10 @@ class DateRangePickerElement extends WrappedElement {
min-width: unset !important;
display: block !important;
}
:host([opens-vertical="up"]) .daterangepicker {
bottom: 100%;
top: auto !important;
}
`;
if (mainWindow.document.dir === "rtl") {
style.innerHTML += `
@@ -340,4 +353,7 @@ declare global {
interface HTMLElementTagNameMap {
"date-range-picker": DateRangePickerElement;
}
interface HASSDomEvents {
toggle: { open: boolean };
}
}

View File

@@ -1,8 +1,9 @@
import { consume } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { fullEntitiesContext } from "../../data/context";
import type { DeviceAutomation } from "../../data/device/device_automation";
import {
@@ -11,11 +12,12 @@ import {
} from "../../data/device/device_automation";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { HomeAssistant } from "../../types";
import "../ha-generic-picker";
import "../ha-md-select";
import "../ha-md-select-option";
import type { PickerValueRenderer } from "../ha-picker-field";
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
export abstract class HaDeviceAutomationPicker<
T extends DeviceAutomation,
@@ -28,7 +30,7 @@ export abstract class HaDeviceAutomationPicker<
@property({ type: Object }) public value?: T;
@state() private _automations: T[] = [];
@state() private _automations?: T[];
// Trigger an empty render so we start with a clean DOM.
// paper-listbox does not like changing things around.
@@ -44,12 +46,6 @@ export abstract class HaDeviceAutomationPicker<
);
}
protected get UNKNOWN_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.unknown_action"
);
}
private _localizeDeviceAutomation: (
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
@@ -75,7 +71,7 @@ export abstract class HaDeviceAutomationPicker<
}
private get _value() {
if (!this.value) {
if (!this.value || !this._automations) {
return "";
}
@@ -88,7 +84,7 @@ export abstract class HaDeviceAutomationPicker<
);
if (idx === -1) {
return UNKNOWN_AUTOMATION_KEY;
return this.value.alias || this.value.type || "unknown";
}
return `${this._automations[idx].device_id}_${idx}`;
@@ -99,37 +95,21 @@ export abstract class HaDeviceAutomationPicker<
return nothing;
}
const value = this._value;
return html`
<ha-md-select
.label=${this.label}
.value=${value}
@change=${this._automationChanged}
@closed=${stopPropagation}
.disabled=${this._automations.length === 0}
>
${value === NO_AUTOMATION_KEY
? html`<ha-md-select-option .value=${NO_AUTOMATION_KEY}>
${this.NO_AUTOMATION_TEXT}
</ha-md-select-option>`
: nothing}
${value === UNKNOWN_AUTOMATION_KEY
? html`<ha-md-select-option .value=${UNKNOWN_AUTOMATION_KEY}>
${this.UNKNOWN_AUTOMATION_TEXT}
</ha-md-select-option>`
: nothing}
${this._automations.map(
(automation, idx) => html`
<ha-md-select-option .value=${`${automation.device_id}_${idx}`}>
${this._localizeDeviceAutomation(
this.hass,
this._entityReg,
automation
)}
</ha-md-select-option>
`
)}
</ha-md-select>
`;
return html`<ha-generic-picker
.hass=${this.hass}
.label=${this.label}
.value=${value}
.disabled=${!this._automations || this._automations.length === 0}
.getItems=${this._getItems(value, this._automations)}
@value-changed=${this._automationChanged}
.valueRenderer=${this._valueRenderer}
.unknownItemText=${this.hass.localize(
"ui.panel.config.devices.automation.actions.unknown_action"
)}
hide-clear-icon
>
</ha-generic-picker>`;
}
protected updated(changedProps) {
@@ -140,6 +120,57 @@ export abstract class HaDeviceAutomationPicker<
}
}
private _getItems = memoizeOne(
(value: string, automations: T[] | undefined) => {
if (!automations) {
return () => undefined;
}
const automationListItems = automations.map((automation, idx) => {
const primary = this._localizeDeviceAutomation(
this.hass,
this._entityReg,
automation
);
return {
id: `${automation.device_id}_${idx}`,
primary,
};
});
automationListItems.sort((a, b) =>
caseInsensitiveStringCompare(
a.primary,
b.primary,
this.hass.locale.language
)
);
if (value === NO_AUTOMATION_KEY) {
automationListItems.unshift({
id: NO_AUTOMATION_KEY,
primary: this.NO_AUTOMATION_TEXT,
});
}
return () => automationListItems;
}
);
private _valueRenderer: PickerValueRenderer = (value: string) => {
const automation = this._automations?.find(
(a, idx) => value === `${a.device_id}_${idx}`
);
const text = automation
? this._localizeDeviceAutomation(this.hass, this._entityReg, automation)
: value === NO_AUTOMATION_KEY
? this.NO_AUTOMATION_TEXT
: value;
return html`<span slot="headline">${text}</span>`;
};
private async _updateDeviceInfo() {
this._automations = this.deviceId
? (await this._fetchDeviceAutomations(this.hass, this.deviceId)).sort(
@@ -161,13 +192,14 @@ export abstract class HaDeviceAutomationPicker<
this._renderEmpty = false;
}
private _automationChanged(ev) {
const value = ev.target.value;
if (!value || [UNKNOWN_AUTOMATION_KEY, NO_AUTOMATION_KEY].includes(value)) {
private _automationChanged(ev: CustomEvent<{ value: string }>) {
ev.stopPropagation();
const value = ev.detail.value;
if (!value || NO_AUTOMATION_KEY === value) {
return;
}
const [deviceId, idx] = value.split("_");
const automation = this._automations[idx];
const automation = this._automations![idx];
if (automation.device_id !== deviceId) {
return;
}

View File

@@ -1,4 +1,4 @@
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@@ -18,6 +18,7 @@ import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import "../ha-generic-picker";
import type { HaGenericPicker } from "../ha-generic-picker";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry
@@ -94,7 +95,30 @@ export class HaDevicePicker extends LitElement {
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
private _getDevicesMemoized = memoizeOne(getDevices);
private _getDevicesMemoized = memoizeOne(
(
_devices: HomeAssistant["devices"],
configEntryLookup: Record<string, ConfigEntry>,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeDevices?: string[],
value?: string
) =>
getDevices(
this.hass,
configEntryLookup,
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeDevices,
value
)
);
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
@@ -110,7 +134,7 @@ export class HaDevicePicker extends LitElement {
private _getItems = () =>
this._getDevicesMemoized(
this.hass,
this.hass.devices,
this._configEntryLookup,
this.includeDomains,
this.excludeDomains,
@@ -162,7 +186,7 @@ export class HaDevicePicker extends LitElement {
}
);
private _rowRenderer: ComboBoxLitRenderer<DevicePickerItem> = (item) => html`
private _rowRenderer: RenderItemFunction<DevicePickerItem> = (item) => html`
<ha-combo-box-item type="button">
${item.domain
? html`
@@ -205,6 +229,8 @@ export class HaDevicePicker extends LitElement {
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
.disabled=${this.disabled}
.helper=${this.helper}
.label=${this.label}
.searchLabel=${this.searchLabel}
.notFoundLabel=${this._notFoundLabel}

View File

@@ -61,7 +61,6 @@ class HaDevicesPicker extends LitElement {
(entityId) => html`
<div>
<ha-device-picker
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
.deviceFilter=${this.deviceFilter}
@@ -79,7 +78,6 @@ class HaDevicesPicker extends LitElement {
)}
<div>
<ha-device-picker
allow-custom-entity
.hass=${this.hass}
.helper=${this.helper}
.deviceFilter=${this.deviceFilter}

View File

@@ -99,7 +99,6 @@ class HaEntitiesPicker extends LitElement {
(entityId) => html`
<div class="entity">
<ha-entity-picker
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
.includeDomains=${this.includeDomains}
@@ -129,7 +128,6 @@ class HaEntitiesPicker extends LitElement {
</ha-sortable>
<div>
<ha-entity-picker
allow-custom-entity
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}

View File

@@ -1,16 +1,11 @@
import type { PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
interface AttributeOption {
value: string;
label: string;
}
import "../ha-generic-picker";
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
@customElement("ha-entity-attribute-picker")
class HaEntityAttributePicker extends LitElement {
@@ -42,51 +37,44 @@ class HaEntityAttributePicker extends LitElement {
@property() public helper?: string;
@state() private _opened = false;
private _getItemsMemoized = memoizeOne(
(
entityId: string | string[] | undefined,
hideAttributes: string[] | undefined,
hass: HomeAssistant
): PickerComboBoxItem[] => {
const entityIds = entityId ? ensureArray(entityId) : [];
const options: PickerComboBoxItem[] = [];
const optionsSet = new Set<string>();
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
protected shouldUpdate(changedProps: PropertyValues) {
return !(!changedProps.has("_opened") && this._opened);
}
protected updated(changedProps: PropertyValues) {
if (
(changedProps.has("_opened") && this._opened) ||
changedProps.has("entityId") ||
changedProps.has("attribute")
) {
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
const entitiesOptions = entityIds.map<AttributeOption[]>((entityId) => {
const stateObj = this.hass.states[entityId];
for (const id of entityIds) {
const stateObj = hass.states[id];
if (!stateObj) {
return [];
continue;
}
const attributes = Object.keys(stateObj.attributes).filter(
(a) => !this.hideAttributes?.includes(a)
(a) => !hideAttributes?.includes(a)
);
return attributes.map((a) => ({
value: a,
label: this.hass.formatEntityAttributeName(stateObj, a),
}));
});
const options: AttributeOption[] = [];
const optionsSet = new Set<string>();
for (const entityOptions of entitiesOptions) {
for (const option of entityOptions) {
if (!optionsSet.has(option.value)) {
optionsSet.add(option.value);
options.push(option);
for (const attribute of attributes) {
if (!optionsSet.has(attribute)) {
optionsSet.add(attribute);
options.push({
id: attribute,
primary: hass.formatEntityAttributeName(stateObj, attribute),
sorting_label: attribute,
});
}
}
}
(this._comboBox as any).filteredItems = options;
return options;
}
}
);
private _getItems = () =>
this._getItemsMemoized(this.entityId, this.hideAttributes, this.hass);
protected render() {
if (!this.hass) {
@@ -94,10 +82,9 @@ class HaEntityAttributePicker extends LitElement {
}
return html`
<ha-combo-box
<ha-generic-picker
.hass=${this.hass}
.value=${this.value}
.autofocus=${this.autofocus}
.label=${this.label ??
this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute"
@@ -106,38 +93,21 @@ class HaEntityAttributePicker extends LitElement {
.required=${this.required}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomValue}
item-id-path="value"
item-value-path="value"
item-label-path="label"
@opened-changed=${this._openedChanged}
.getItems=${this._getItems}
@value-changed=${this._valueChanged}
>
</ha-combo-box>
</ha-generic-picker>
`;
}
private get _value() {
return this.value || "";
}
private _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _valueChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
if (newValue !== this.value) {
this.value = newValue;
fireEvent(this, "value-changed", { value: newValue });
fireEvent(this, "change");
}, 0);
}
}
}

View File

@@ -1,15 +1,11 @@
import "@material/mwc-menu/mwc-menu-surface";
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import type { EntityNameItem } from "../../common/entity/compute_entity_name_display";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import type { EntityNameType } from "../../common/translations/entity-state";
@@ -18,20 +14,15 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../chips/ha-assist-chip";
import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-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 "../ha-sortable";
interface EntityNameOption {
primary: string;
secondary?: string;
field_label: string;
value: string;
}
const rowRenderer: ComboBoxLitRenderer<EntityNameOption> = (item) => html`
<ha-combo-box-item type="button">
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
<ha-combo-box-item type="button" compact>
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
@@ -79,11 +70,7 @@ export class HaEntityNamePicker extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
@query(".container", true) private _container?: HTMLDivElement;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
@state() private _opened = false;
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
private _editIndex?: number;
@@ -115,7 +102,7 @@ export class HaEntityNamePicker extends LitElement {
return options;
});
private _getOptions = memoizeOne((entityId?: string) => {
private _getItems = memoizeOne((entityId?: string) => {
if (!entityId) {
return [];
}
@@ -124,7 +111,7 @@ export class HaEntityNamePicker extends LitElement {
const items = (
["entity", "device", "area", "floor"] as const
).map<EntityNameOption>((name) => {
).map<PickerComboBoxItem>((name) => {
const stateObj = this.hass.states[entityId];
const isValid = types.has(name);
const primary = this.hass.localize(
@@ -137,25 +124,39 @@ export class HaEntityNamePicker extends LitElement {
`ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys
)) || "-";
const id = formatOptionValue({ type: name });
return {
id,
primary,
secondary,
field_label: primary,
value: formatOptionValue({ type: name }),
search_labels: {
primary,
secondary: secondary || null,
id,
},
sorting_label: primary,
};
});
return items;
});
private _customNameOption = memoizeOne((text: string) => ({
primary: this.hass.localize(
"ui.components.entity.entity-name-picker.custom_name"
),
secondary: `"${text}"`,
field_label: text,
value: formatOptionValue({ type: "text", text }),
}));
private _customNameOption = memoizeOne(
(text: string): PickerComboBoxItem => ({
id: formatOptionValue({ type: "text", text }),
primary: this.hass.localize(
"ui.components.entity.entity-name-picker.custom_name"
),
secondary: `"${text}"`,
search_labels: {
primary: text,
secondary: `"${text}"`,
id: formatOptionValue({ type: "text", text }),
},
sorting_label: text,
})
);
private _formatItem = (item: EntityNameItem) => {
if (item.type === "text") {
@@ -171,88 +172,79 @@ export class HaEntityNamePicker extends LitElement {
protected render() {
const value = this._items;
const options = this._getOptions(this.entityId);
const validTypes = this._validTypes(this.entityId);
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<div class="container">
<ha-sortable
no-style
@item-moved=${this._moveItem}
.disabled=${this.disabled}
handle-selector="button.primary.action"
filter=".add"
>
<ha-chip-set>
${repeat(
this._items,
(item) => item,
(item: EntityNameItem, idx) => {
const label = this._formatItem(item);
const isValid = validTypes.has(item.type);
return html`
<ha-input-chip
data-idx=${idx}
@remove=${this._removeItem}
@click=${this._editItem}
.label=${label}
.selected=${!this.disabled}
.disabled=${this.disabled}
class=${!isValid ? "invalid" : ""}
>
<ha-svg-icon
slot="icon"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
<span>${label}</span>
</ha-input-chip>
`;
}
)}
${this.disabled
? nothing
: html`
<ha-assist-chip
@click=${this._addItem}
.disabled=${this.disabled}
label=${this.hass.localize(
"ui.components.entity.entity-name-picker.add"
)}
class="add"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-assist-chip>
`}
</ha-chip-set>
</ha-sortable>
<mwc-menu-surface
.open=${this._opened}
@closed=${this._onClosed}
@opened=${this._onOpened}
@input=${stopPropagation}
.anchor=${this._container}
>
<ha-combo-box
.hass=${this.hass}
.value=${""}
.autofocus=${this.autofocus}
<ha-generic-picker
.hass=${this.hass}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.getItems=${this._getFilteredItems}
.rowRenderer=${rowRenderer}
.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
no-style
@item-moved=${this._moveItem}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.items=${options}
allow-custom-value
item-id-path="value"
item-value-path="value"
item-label-path="field_label"
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._comboBoxValueChanged}
@filter-changed=${this._filterChanged}
handle-selector="button.primary.action"
filter=".add"
>
</ha-combo-box>
</mwc-menu-surface>
</div>
<ha-chip-set>
${repeat(
this._items,
(item) => item,
(item: EntityNameItem, idx) => {
const label = this._formatItem(item);
const isValid = validTypes.has(item.type);
return html`
<ha-input-chip
data-idx=${idx}
@remove=${this._removeItem}
@click=${this._editItem}
.label=${label}
.selected=${!this.disabled}
.disabled=${this.disabled}
class=${!isValid ? "invalid" : ""}
>
<ha-svg-icon
slot="icon"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
<span>${label}</span>
</ha-input-chip>
`;
}
)}
${this.disabled
? nothing
: html`
<ha-assist-chip
@click=${this._addItem}
.disabled=${this.disabled}
label=${this.hass.localize(
"ui.components.entity.entity-name-picker.add"
)}
class="add"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-assist-chip>
`}
</ha-chip-set>
</ha-sortable>
</div>
</ha-generic-picker>
${this._renderHelper()}
`;
}
@@ -267,32 +259,27 @@ export class HaEntityNamePicker extends LitElement {
: nothing;
}
private _onClosed(ev) {
private async _addItem(ev: Event) {
ev.stopPropagation();
this._opened = false;
this._editIndex = undefined;
await this.updateComplete;
await this._picker?.open();
}
private async _onOpened(ev) {
if (!this._opened) {
return;
}
private async _editItem(ev: Event) {
ev.stopPropagation();
this._opened = true;
await this._comboBox?.focus();
await this._comboBox?.open();
}
private async _addItem(ev) {
ev.stopPropagation();
this._opened = true;
}
private async _editItem(ev) {
ev.stopPropagation();
const idx = parseInt(ev.currentTarget.dataset.idx, 10);
const idx = parseInt(
(ev.currentTarget as HTMLElement).dataset.idx || "",
10
);
this._editIndex = idx;
this._opened = true;
await this.updateComplete;
await this._picker?.open();
const value = this._items[idx];
// Pre-fill the field value when editing a text item
if (value.type === "text" && value.text) {
this._picker?.setFieldValue(value.text);
}
}
private get _items(): EntityNameItem[] {
@@ -322,78 +309,55 @@ export class HaEntityNamePicker extends LitElement {
}
);
private _openedChanged(ev: ValueChangedEvent<boolean>) {
const open = ev.detail.value;
if (open) {
const options = this._comboBox.items || [];
const initialItem =
this._editIndex != null ? this._items[this._editIndex] : undefined;
const initialValue = initialItem ? formatOptionValue(initialItem) : "";
const filteredItems = this._filterSelectedOptions(options, initialValue);
if (initialItem?.type === "text" && initialItem.text) {
filteredItems.push(this._customNameOption(initialItem.text));
}
this._comboBox.filteredItems = filteredItems;
this._comboBox.setInputValue(initialValue);
} else {
this._opened = false;
this._comboBox.setInputValue("");
private _getPickerValue(): string | undefined {
if (this._editIndex != null) {
const item = this._items[this._editIndex];
return item ? formatOptionValue(item) : undefined;
}
return undefined;
}
private _filterSelectedOptions = (
options: EntityNameOption[],
current?: string
) => {
const items = this._items;
private _getFilteredItems = (): PickerComboBoxItem[] => {
const items = this._getItems(this.entityId);
const currentItem =
this._editIndex != null ? this._items[this._editIndex] : undefined;
const currentValue = currentItem ? formatOptionValue(currentItem) : "";
const excludedValues = new Set(
items
this._items
.filter((item) => UNIQUE_TYPES.has(item.type))
.map((item) => formatOptionValue(item))
);
const filteredOptions = options.filter(
(option) => !excludedValues.has(option.value) || option.value === current
const filteredItems = items.filter(
(item) => !excludedValues.has(item.id) || item.id === currentValue
);
return filteredOptions;
};
private _filterChanged(ev: ValueChangedEvent<string>) {
const input = ev.detail.value;
const filter = input?.toLowerCase() || "";
const options = this._comboBox.items || [];
const currentItem =
this._editIndex != null ? this._items[this._editIndex] : undefined;
const currentValue = currentItem ? formatOptionValue(currentItem) : "";
let filteredItems = this._filterSelectedOptions(options, currentValue);
if (!filter) {
this._comboBox.filteredItems = filteredItems;
return;
// When editing an existing text item, include it in the base items
if (currentItem?.type === "text" && currentItem.text) {
filteredItems.push(this._customNameOption(currentItem.text));
}
const fuseOptions: IFuseOptions<EntityNameOption> = {
keys: ["primary", "secondary", "value"],
isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
ignoreDiacritics: true,
};
return filteredItems;
};
const fuse = new Fuse(filteredItems, fuseOptions);
filteredItems = fuse.search(filter).map((result) => result.item);
filteredItems.push(this._customNameOption(input));
this._comboBox.filteredItems = filteredItems;
}
private _searchFn = (
searchString: string,
filteredItems: PickerComboBoxItem[]
): PickerComboBoxItem[] => {
const currentItem =
this._editIndex != null ? this._items[this._editIndex] : undefined;
const currentId =
currentItem?.type === "text" && currentItem.text
? this._customNameOption(currentItem.text).id
: undefined;
// Remove custom name option if search string is present to avoid duplicates
if (searchString && currentId) {
return filteredItems.filter((item) => item.id !== currentId);
}
return filteredItems;
};
private async _moveItem(ev: CustomEvent) {
ev.stopPropagation();
@@ -403,25 +367,21 @@ export class HaEntityNamePicker extends LitElement {
const element = newValue.splice(oldIndex, 1)[0];
newValue.splice(newIndex, 0, element);
this._setValue(newValue);
await this.updateComplete;
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
}
private async _removeItem(ev) {
private async _removeItem(ev: Event) {
ev.stopPropagation();
const value = [...this._items];
const idx = parseInt(ev.target.dataset.idx, 10);
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
value.splice(idx, 1);
this._setValue(value);
await this.updateComplete;
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
}
private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
ev.stopPropagation();
const value = ev.detail.value;
if (this.disabled || value === "") {
if (this.disabled || !value) {
return;
}
@@ -431,11 +391,16 @@ export class HaEntityNamePicker extends LitElement {
if (this._editIndex != null) {
newValue[this._editIndex] = item;
this._editIndex = undefined;
} else {
newValue.push(item);
}
this._setValue(newValue);
if (this._picker) {
this._picker.value = undefined;
}
}
private _setValue(value: EntityNameItem[]) {
@@ -497,10 +462,6 @@ export class HaEntityNamePicker extends LitElement {
order: 1;
}
mwc-menu-surface {
--mdc-menu-min-width: 100%;
}
ha-chip-set {
padding: var(--ha-space-2) var(--ha-space-2);
}

View File

@@ -1,5 +1,5 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiPlus, mdiShape } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -172,9 +172,9 @@ export class HaEntityPicker extends LitElement {
return this.showEntityId || this.hass.userData?.showEntityIdPicker;
}
private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = (
private _rowRenderer: RenderItemFunction<EntityComboBoxItem> = (
item,
{ index }
index
) => {
const showEntityId = this._showEntityId;
@@ -277,12 +277,13 @@ export class HaEntityPicker extends LitElement {
.disabled=${this.disabled}
.autofocus=${this.autofocus}
.allowCustomValue=${this.allowCustomEntity}
.required=${this.required}
.label=${this.label}
.placeholder=${placeholder}
.helper=${this.helper}
.value=${this.addButton ? undefined : this.value}
.searchLabel=${this.searchLabel}
.notFoundLabel=${this._notFoundLabel}
.placeholder=${placeholder}
.value=${this.addButton ? undefined : this.value}
.rowRenderer=${this._rowRenderer}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}
@@ -290,6 +291,7 @@ export class HaEntityPicker extends LitElement {
.searchFn=${this._searchFn}
.valueRenderer=${this._valueRenderer}
.searchKeys=${entityComboBoxKeys}
use-top-label
.addButtonLabel=${this.addButton
? this.hass.localize("ui.components.entity.entity-picker.add")
: undefined}

View File

@@ -1,17 +1,13 @@
import "@material/mwc-menu/mwc-menu-surface";
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeDomain } from "../../common/entity/compute_domain";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import {
STATE_DISPLAY_SPECIAL_CONTENT,
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS,
@@ -20,21 +16,16 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../chips/ha-assist-chip";
import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-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 "../ha-sortable";
interface StateContentOption {
primary: string;
value: string;
}
const rowRenderer: ComboBoxLitRenderer<StateContentOption> = (item) => html`
<ha-combo-box-item type="button">
<span slot="headline">${item.primary}</span>
</ha-combo-box-item>
`;
const HIDDEN_ATTRIBUTES = [
"access_token",
"available_modes",
@@ -105,69 +96,148 @@ export class HaStateContentPicker extends LitElement {
@property({ type: Boolean, attribute: "allow-name" }) public allowName =
false;
@property({ type: Boolean, attribute: "allow-context" }) public allowContext =
false;
@property() public label?: string;
@property() public value?: string[] | string;
@property() public helper?: string;
@query(".container", true) private _container?: HTMLDivElement;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
@state() private _opened = false;
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
private _editIndex?: number;
private _options = memoizeOne(
(entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
private _getItems = memoizeOne(
(
entityId?: string,
stateObj?: HassEntity,
allowName?: boolean,
allowContext?: boolean
) => {
const domain = entityId ? computeDomain(entityId) : undefined;
return [
const items: PickerComboBoxItem[] = [
{
id: "state",
primary: this.hass.localize(
"ui.components.state-content-picker.state"
),
value: "state",
sorting_label: this.hass.localize(
"ui.components.state-content-picker.state"
),
},
...(allowName
? [
{
id: "name",
primary: this.hass.localize(
"ui.components.state-content-picker.name"
),
value: "name",
},
sorting_label: this.hass.localize(
"ui.components.state-content-picker.name"
),
} satisfies PickerComboBoxItem,
]
: []),
{
id: "last_changed",
primary: this.hass.localize(
"ui.components.state-content-picker.last_changed"
),
value: "last_changed",
sorting_label: this.hass.localize(
"ui.components.state-content-picker.last_changed"
),
},
{
id: "last_updated",
primary: this.hass.localize(
"ui.components.state-content-picker.last_updated"
),
value: "last_updated",
sorting_label: this.hass.localize(
"ui.components.state-content-picker.last_updated"
),
},
...(allowContext && stateObj
? (() => {
const context = getEntityContext(
stateObj,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const contextItems: PickerComboBoxItem[] = [];
if (context.device) {
contextItems.push({
id: "device_name",
primary: this.hass.localize(
"ui.components.state-content-picker.device_name"
),
sorting_label: this.hass.localize(
"ui.components.state-content-picker.device_name"
),
});
}
if (context.area) {
contextItems.push({
id: "area_name",
primary: this.hass.localize(
"ui.components.state-content-picker.area_name"
),
sorting_label: this.hass.localize(
"ui.components.state-content-picker.area_name"
),
});
}
if (context.floor) {
contextItems.push({
id: "floor_name",
primary: this.hass.localize(
"ui.components.state-content-picker.floor_name"
),
sorting_label: this.hass.localize(
"ui.components.state-content-picker.floor_name"
),
});
}
return contextItems;
})()
: []),
...(domain
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
).map((content) => ({
primary: this.hass.localize(
`ui.components.state-content-picker.${content}`
),
value: content,
}))
).map(
(content) =>
({
id: content,
primary: this.hass.localize(
`ui.components.state-content-picker.${content}`
),
sorting_label: this.hass.localize(
`ui.components.state-content-picker.${content}`
),
}) satisfies PickerComboBoxItem
)
: []),
...Object.keys(stateObj?.attributes ?? {})
.filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
.map((attribute) => ({
primary: this.hass.formatEntityAttributeName(stateObj!, attribute),
value: attribute,
})),
] satisfies StateContentOption[];
.map(
(attribute) =>
({
id: attribute,
primary: this.hass.formatEntityAttributeName(
stateObj!,
attribute
),
sorting_label: this.hass.formatEntityAttributeName(
stateObj!,
attribute
),
}) satisfies PickerComboBoxItem
),
];
return items;
}
);
@@ -178,122 +248,120 @@ export class HaStateContentPicker extends LitElement {
? this.hass.states[this.entityId]
: undefined;
const options = this._options(this.entityId, stateObj, this.allowName);
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<div class="container ${this.disabled ? "disabled" : ""}">
<ha-sortable
no-style
@item-moved=${this._moveItem}
.disabled=${this.disabled}
handle-selector="button.primary.action"
filter=".add"
>
<ha-chip-set>
${repeat(
this._value,
(item) => item,
(item: string, idx) => {
const label = options.find((o) => o.value === item)?.primary;
const isValid = !!label;
return html`
<ha-input-chip
data-idx=${idx}
@remove=${this._removeItem}
@click=${this._editItem}
.label=${label || item}
.selected=${!this.disabled}
.disabled=${this.disabled}
class=${!isValid ? "invalid" : ""}
>
<ha-svg-icon
slot="icon"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</ha-input-chip>
`;
}
)}
${this.disabled
? nothing
: html`
<ha-assist-chip
@click=${this._addItem}
.disabled=${this.disabled}
label=${this.hass.localize(
"ui.components.entity.entity-state-content-picker.add"
)}
class="add"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-assist-chip>
`}
</ha-chip-set>
</ha-sortable>
<mwc-menu-surface
.open=${this._opened}
@closed=${this._onClosed}
@opened=${this._onOpened}
@input=${stopPropagation}
.anchor=${this._container}
>
<ha-combo-box
.hass=${this.hass}
.value=${""}
.autofocus=${this.autofocus}
.disabled=${this.disabled || !this.entityId}
.required=${this.required && !value.length}
.helper=${this.helper}
.items=${options}
allow-custom-value
item-id-path="value"
item-value-path="value"
item-label-path="primary"
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._comboBoxValueChanged}
@filter-changed=${this._filterChanged}
<ha-generic-picker
.hass=${this.hass}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.value=${this._getPickerValue()}
.getItems=${this._getFilteredItems}
.getAdditionalItems=${this._getAdditionalItems}
.searchFn=${this._searchFn}
@value-changed=${this._pickerValueChanged}
>
<div slot="field" class="container">
<ha-sortable
no-style
@item-moved=${this._moveItem}
.disabled=${this.disabled}
handle-selector="button.primary.action"
filter=".add"
>
</ha-combo-box>
</mwc-menu-surface>
</div>
<ha-chip-set>
${repeat(
this._value,
(item) => item,
(item: string, idx) => {
const label = this._getItemLabel(item, stateObj);
const isValid = !!label;
return html`
<ha-input-chip
data-idx=${idx}
@remove=${this._removeItem}
@click=${this._editItem}
.label=${label || item}
.selected=${!this.disabled}
.disabled=${this.disabled}
class=${!isValid ? "invalid" : ""}
>
<ha-svg-icon
slot="icon"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</ha-input-chip>
`;
}
)}
${this.disabled
? nothing
: html`
<ha-assist-chip
@click=${this._addItem}
.disabled=${this.disabled}
label=${this.hass.localize(
"ui.components.entity.entity-state-content-picker.add"
)}
class="add"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-assist-chip>
`}
</ha-chip-set>
</ha-sortable>
</div>
</ha-generic-picker>
${this._renderHelper()}
`;
}
private _onClosed(ev) {
private _renderHelper() {
return this.helper
? html`
<ha-input-helper-text .disabled=${this.disabled}>
${this.helper}
</ha-input-helper-text>
`
: nothing;
}
private async _addItem(ev: Event) {
ev.stopPropagation();
this._opened = false;
this._editIndex = undefined;
await this.updateComplete;
await this._picker?.open();
}
private async _onOpened(ev) {
if (!this._opened) {
return;
}
private async _editItem(ev: Event) {
ev.stopPropagation();
this._opened = true;
await this._comboBox?.focus();
await this._comboBox?.open();
}
private async _addItem(ev) {
ev.stopPropagation();
this._opened = true;
}
private async _editItem(ev) {
ev.stopPropagation();
const idx = parseInt(ev.currentTarget.dataset.idx, 10);
const idx = parseInt(
(ev.currentTarget as HTMLElement).dataset.idx || "",
10
);
this._editIndex = idx;
this._opened = true;
await this.updateComplete;
await this._picker?.open();
}
private get _value() {
return !this.value ? [] : ensureArray(this.value);
}
private _getItemLabel = memoizeOne(
(value: string, stateObj?: HassEntity): string | undefined => {
const stateObjForItems = this.entityId
? this.hass.states[this.entityId]
: stateObj;
const items = this._getItems(
this.entityId,
stateObjForItems,
this.allowName,
this.allowContext
);
return items.find((item) => item.id === value)?.primary;
}
);
private _toValue = memoizeOne((value: string[]): typeof this.value => {
if (value.length === 0) {
return undefined;
@@ -304,63 +372,91 @@ export class HaStateContentPicker extends LitElement {
return value;
});
private _openedChanged(ev: ValueChangedEvent<boolean>) {
const open = ev.detail.value;
if (open) {
const options = this._comboBox.items || [];
const initialValue =
this._editIndex != null ? this._value[this._editIndex] : "";
const filteredItems = this._filterSelectedOptions(options, initialValue);
this._comboBox.filteredItems = filteredItems;
this._comboBox.setInputValue(initialValue);
} else {
this._opened = false;
private _getPickerValue(): string | undefined {
if (this._editIndex != null) {
return this._value[this._editIndex];
}
return undefined;
}
private _filterSelectedOptions = (
options: StateContentOption[],
current?: string
) => {
private _customValueOption = memoizeOne(
(text: string): PickerComboBoxItem => ({
id: text,
primary: this.hass.localize(
"ui.components.entity.entity-state-content-picker.custom_attribute"
),
secondary: `"${text}"`,
search_labels: {
primary: text,
secondary: `"${text}"`,
id: text,
},
sorting_label: text,
})
);
private _getFilteredItems = (): PickerComboBoxItem[] => {
const stateObj = this.entityId
? this.hass.states[this.entityId]
: undefined;
const items = this._getItems(
this.entityId,
stateObj,
this.allowName,
this.allowContext
);
const currentValue =
this._editIndex != null ? this._value[this._editIndex] : undefined;
const value = this._value;
return options.filter(
(option) => !value.includes(option.value) || option.value === current
);
};
private _filterChanged(ev: ValueChangedEvent<string>) {
const input = ev.detail.value;
const filter = input?.toLowerCase() || "";
const options = this._comboBox.items || [];
const currentValue =
this._editIndex != null ? this._value[this._editIndex] : "";
this._comboBox.filteredItems = this._filterSelectedOptions(
options,
currentValue
const filteredItems = items.filter(
(item) => !value.includes(item.id) || item.id === currentValue
);
if (!filter) {
return;
// When editing an existing custom value, include it in the base items
if (currentValue && !items.find((item) => item.id === currentValue)) {
filteredItems.push(this._customValueOption(currentValue));
}
const fuseOptions: IFuseOptions<StateContentOption> = {
keys: ["primary", "secondary", "value"],
isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
ignoreDiacritics: true,
};
return filteredItems;
};
const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions);
const filteredItems = fuse.search(filter).map((result) => result.item);
private _getAdditionalItems = (
searchString?: string
): PickerComboBoxItem[] => {
const stateObj = this.entityId
? this.hass.states[this.entityId]
: undefined;
const items = this._getItems(
this.entityId,
stateObj,
this.allowName,
this.allowContext
);
this._comboBox.filteredItems = filteredItems;
}
// 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();
@@ -370,25 +466,21 @@ export class HaStateContentPicker extends LitElement {
const element = newValue.splice(oldIndex, 1)[0];
newValue.splice(newIndex, 0, element);
this._setValue(newValue);
await this.updateComplete;
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
}
private async _removeItem(ev) {
private async _removeItem(ev: Event) {
ev.stopPropagation();
const value = [...this._value];
const idx = parseInt(ev.target.dataset.idx, 10);
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
value.splice(idx, 1);
this._setValue(value);
await this.updateComplete;
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
}
private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
ev.stopPropagation();
const value = ev.detail.value;
if (this.disabled || value === "") {
if (this.disabled || !value) {
return;
}
@@ -396,11 +488,16 @@ export class HaStateContentPicker extends LitElement {
if (this._editIndex != null) {
newValue[this._editIndex] = value;
this._editIndex = undefined;
} else {
newValue.push(value);
}
this._setValue(newValue);
if (this._picker) {
this._picker.value = undefined;
}
}
private _setValue(value: string[]) {
@@ -442,7 +539,7 @@ export class HaStateContentPicker extends LitElement {
height 180ms ease-in-out,
background-color 180ms ease-in-out;
}
.container.disabled:after {
:host([disabled]) .container:after {
background-color: var(
--mdc-text-field-disabled-line-color,
rgba(0, 0, 0, 0.42)
@@ -462,10 +559,6 @@ export class HaStateContentPicker extends LitElement {
order: 1;
}
mwc-menu-surface {
--mdc-menu-min-width: 100%;
}
ha-chip-set {
padding: var(--ha-space-2) var(--ha-space-2);
}
@@ -486,6 +579,11 @@ export class HaStateContentPicker extends LitElement {
.sortable-drag {
cursor: grabbing;
}
ha-input-helper-text {
display: block;
margin: var(--ha-space-2) 0 0;
}
`;
}

View File

@@ -1,27 +1,23 @@
import type { PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { getStates } from "../../common/entity/get_states";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
interface StateOption {
value: string;
label: string;
}
import "../ha-generic-picker";
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
import type { PickerValueRenderer } from "../ha-picker-field";
@customElement("ha-entity-state-picker")
class HaEntityStatePicker extends LitElement {
export class HaEntityStatePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId?: string | string[];
@property() public attribute?: string;
@property({ attribute: false }) public extraOptions?: any[];
@property({ attribute: false }) public extraOptions?: PickerComboBoxItem[];
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@@ -42,59 +38,82 @@ class HaEntityStatePicker extends LitElement {
@property() public helper?: string;
@state() private _opened = false;
private _getItems = memoizeOne(
(
hass: HomeAssistant,
entityId: string | string[] | undefined,
attribute: string | undefined,
hideStates: string[] | undefined,
extraOptions: PickerComboBoxItem[] | undefined
): PickerComboBoxItem[] => {
const entityIds = entityId ? ensureArray(entityId) : [];
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
const entitiesOptions = entityIds.map<PickerComboBoxItem[]>(
(entityIdItem) => {
const stateObj = hass.states[entityIdItem] || {
entity_id: entityIdItem,
attributes: {},
};
protected shouldUpdate(changedProps: PropertyValues) {
return !(!changedProps.has("_opened") && this._opened);
}
const states = getStates(hass, stateObj, attribute).filter(
(s) => !hideStates?.includes(s)
);
protected updated(changedProps: PropertyValues) {
if (
(changedProps.has("_opened") && this._opened) ||
changedProps.has("entityId") ||
changedProps.has("attribute") ||
changedProps.has("extraOptions")
) {
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
return states
.map((s) => {
const primary = attribute
? hass.formatEntityAttributeValue(stateObj, attribute, s)
: hass.formatEntityState(stateObj, s);
return {
id: s,
primary,
sorting_label: primary,
};
})
.filter((option) => option.id && option.primary);
}
);
const entitiesOptions = entityIds.map<StateOption[]>((entityId) => {
const stateObj = this.hass.states[entityId] || {
entity_id: entityId,
attributes: {},
};
const states = getStates(this.hass, stateObj, this.attribute).filter(
(s) => !this.hideStates?.includes(s)
);
return states.map((s) => ({
value: s,
label: this.attribute
? this.hass.formatEntityAttributeValue(stateObj, this.attribute, s)
: this.hass.formatEntityState(stateObj, s),
}));
});
const options: StateOption[] = [];
const options: PickerComboBoxItem[] = [];
const optionsSet = new Set<string>();
for (const entityOptions of entitiesOptions) {
for (const option of entityOptions) {
if (!optionsSet.has(option.value)) {
optionsSet.add(option.value);
if (!optionsSet.has(option.id)) {
optionsSet.add(option.id);
options.push(option);
}
}
}
if (this.extraOptions) {
options.unshift(...this.extraOptions);
if (extraOptions) {
// Filter out any extraOptions with empty primary or id fields
const validExtraOptions = extraOptions.filter(
(option) => option.id && option.primary
);
options.unshift(...validExtraOptions);
}
(this._comboBox as any).filteredItems = options;
return options;
}
}
);
private _getFilteredItems = (
_searchString?: string,
_section?: string
): PickerComboBoxItem[] =>
this._getItems(
this.hass,
this.entityId,
this.attribute,
this.hideStates,
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) {
@@ -102,48 +121,40 @@ class HaEntityStatePicker extends LitElement {
}
return html`
<ha-combo-box
<ha-generic-picker
.hass=${this.hass}
.value=${this._value}
.allowCustomValue=${this.allowCustomValue}
.disabled=${this.disabled || !this.entityId}
.autofocus=${this.autofocus}
.required=${this.required}
.label=${this.label ??
this.hass.localize("ui.components.entity.entity-state-picker.state")}
.disabled=${this.disabled || !this.entityId}
.required=${this.required}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomValue}
item-id-path="value"
item-value-path="value"
item-label-path="label"
@opened-changed=${this._openedChanged}
.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"
)}
@value-changed=${this._valueChanged}
>
</ha-combo-box>
</ha-generic-picker>
`;
}
private get _value() {
return this.value || "";
}
private _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _valueChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (newValue !== this._value) {
if (newValue !== this.value) {
this._setValue(newValue);
}
}
private _setValue(value: string) {
private _setValue(value: string | undefined) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}
}

View File

@@ -143,17 +143,19 @@ export class HaEntityToggle extends LitElement {
// Optimistic update.
this._isOn = turnOn;
await this.hass.callService(serviceDomain, service, {
entity_id: this.stateObj.entity_id,
});
setTimeout(async () => {
// If after 2 seconds we have not received a state update
// reset the switch to it's original state.
if (this.stateObj === currentState) {
this._isOn = isOn(this.stateObj);
}
}, 2000);
try {
await this.hass.callService(serviceDomain, service, {
entity_id: this.stateObj.entity_id,
});
} finally {
setTimeout(async () => {
// If after 2 seconds we have not received a state update
// reset the switch to it's original state.
if (this.stateObj === currentState) {
this._isOn = isOn(this.stateObj);
}
}, 2000);
}
}
static styles = css`

View File

@@ -1,5 +1,5 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiChartLine, mdiHelpCircle, mdiShape } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators";
@@ -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) {
@@ -424,9 +425,9 @@ export class HaStatisticPicker extends LitElement {
};
}
private _rowRenderer: ComboBoxLitRenderer<StatisticComboBoxItem> = (
private _rowRenderer: RenderItemFunction<StatisticComboBoxItem> = (
item,
{ index }
index
) => {
const showEntityId = this.hass.userData?.showEntityIdPicker;
return html`
@@ -471,13 +472,15 @@ export class HaStatisticPicker extends LitElement {
.hass=${this.hass}
.autofocus=${this.autofocus}
.allowCustomValue=${this.allowCustomEntity}
.disabled=${this.disabled}
.label=${this.label}
use-top-label
.placeholder=${placeholder}
.value=${this.value}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize(
"ui.components.statistic-picker.no_statistics"
)}
.placeholder=${placeholder}
.value=${this.value}
.rowRenderer=${this._rowRenderer}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}

View File

@@ -1,8 +1,8 @@
import { mdiClose } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { HomeAssistant } from "../types";
import { listenMediaQuery } from "../common/dom/media_query";
import type { HomeAssistant } from "../types";
import "./ha-bottom-sheet";
import "./ha-dialog-header";
import "./ha-icon-button";
@@ -88,6 +88,9 @@ export class HaAdaptiveDialog extends LitElement {
@property({ type: Boolean, attribute: "block-mode-change" })
public blockModeChange = false;
@property({ type: Boolean, attribute: "without-header" })
public withoutHeader = false;
@state() private _mode: DialogSheetMode = "dialog";
private _unsubMediaQuery?: () => void;
@@ -118,27 +121,33 @@ export class HaAdaptiveDialog extends LitElement {
if (this._mode === "bottom-sheet") {
return html`
<ha-bottom-sheet .open=${this.open} flexcontent>
<ha-dialog-header
slot="header"
.subtitlePosition=${this.headerSubtitlePosition}
>
<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-drawer="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button>
</slot>
${this.headerTitle !== undefined
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
${this.headerTitle}
</span>`
: html`<slot name="headerTitle" slot="title"></slot>`}
${this.headerSubtitle !== undefined
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
<slot name="headerActionItems" slot="actionItems"></slot>
</ha-dialog-header>
${!this.withoutHeader
? html`<ha-dialog-header
slot="header"
.subtitlePosition=${this.headerSubtitlePosition}
>
<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-drawer="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button>
</slot>
${this.headerTitle !== undefined
? html`<span
slot="title"
class="title"
id="ha-wa-dialog-title"
>
${this.headerTitle}
</span>`
: html`<slot name="headerTitle" slot="title"></slot>`}
${this.headerSubtitle !== undefined
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
<slot name="headerActionItems" slot="actionItems"></slot>
</ha-dialog-header>`
: nothing}
<slot></slot>
<slot name="footer" slot="footer"></slot>
</ha-bottom-sheet>
@@ -156,8 +165,15 @@ export class HaAdaptiveDialog extends LitElement {
.headerSubtitle=${this.headerSubtitle}
.headerSubtitlePosition=${this.headerSubtitlePosition}
flexcontent
.withoutHeader=${this.withoutHeader}
>
<slot name="headerNavigationIcon" slot="headerNavigationIcon"></slot>
<slot name="headerNavigationIcon" slot="headerNavigationIcon">
<ha-icon-button
data-dialog="close"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
</slot>
<slot name="headerTitle" slot="headerTitle"></slot>
<slot name="headerSubtitle" slot="headerSubtitle"></slot>
<slot name="headerActionItems" slot="headerActionItems"></slot>

View File

@@ -1,29 +1,29 @@
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import type { HassioAddonInfo } from "../data/hassio/addon";
import { fetchHassioAddonsInfo } from "../data/hassio/addon";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-alert";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`
const SEARCH_KEYS = [
{ name: "primary", weight: 10 },
{ name: "secondary", weight: 8 },
{ name: "search_labels.description", weight: 6 },
{ name: "search_labels.repository", weight: 5 },
];
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
<ha-combo-box-item type="button">
<span slot="headline">${item.name}</span>
<span slot="supporting-text">${item.slug}</span>
<span slot="headline">${item.primary}</span>
<span slot="supporting-text">${item.secondary}</span>
${item.icon
? html`
<img
alt=""
slot="start"
.src="/api/hassio/addons/${item.slug}/icon"
/>
`
? html` <img alt="" slot="start" .src=${item.icon} /> `
: nothing}
</ha-combo-box-item>
`;
@@ -38,76 +38,93 @@ class HaAddonPicker extends LitElement {
@property() public helper?: string;
@state() private _addons?: HassioAddonInfo[];
@state() private _addons?: PickerComboBoxItem[];
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@query("ha-combo-box") private _comboBox!: HaComboBox;
@query("ha-generic-picker") private _genericPicker!: HaGenericPicker;
@state() private _error?: string;
public open() {
this._comboBox?.open();
this._genericPicker?.open();
}
public focus() {
this._comboBox?.focus();
this._genericPicker?.focus();
}
protected firstUpdated() {
this._getAddons();
this._getApps();
}
protected render() {
const label =
this.label === undefined && this.hass
? this.hass.localize("ui.components.app-picker.app")
: this.label;
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
if (!this._addons) {
return nothing;
}
return html`
<ha-combo-box
<ha-generic-picker
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.addon-picker.addon")
: this.label}
.value=${this._value}
.required=${this.required}
.disabled=${this.disabled}
.autofocus=${this.autofocus}
.label=${label}
.valueRenderer=${this._valueRenderer}
.helper=${this.helper}
.renderer=${rowRenderer}
.items=${this._addons}
item-value-path="slug"
item-id-path="slug"
item-label-path="name"
.disabled=${this.disabled}
.required=${this.required}
.value=${this.value}
.getItems=${this._getItems}
.searchKeys=${SEARCH_KEYS}
.rowRenderer=${rowRenderer}
@value-changed=${this._addonChanged}
></ha-combo-box>
>
</ha-generic-picker>
`;
}
private async _getAddons() {
private async _getApps() {
try {
if (isComponentLoaded(this.hass, "hassio")) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._addons = addonsInfo.addons
.filter((addon) => addon.version)
.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
.map((addon) => ({
id: addon.slug,
primary: addon.name,
secondary: addon.slug,
icon: addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined,
search_labels: {
description: addon.description || null,
repository: addon.repository || null,
},
sorting_label: [addon.name, addon.slug].filter(Boolean).join("_"),
}));
} else {
this._error = this.hass.localize(
"ui.components.addon-picker.error.no_supervisor"
"ui.components.app-picker.error.no_supervisor"
);
}
} catch (_err: any) {
this._error = this.hass.localize(
"ui.components.addon-picker.error.fetch_addons"
"ui.components.app-picker.error.fetch_apps"
);
}
}
private _getItems = () => this._addons!;
private get _value() {
return this.value || "";
}
@@ -128,6 +145,17 @@ class HaAddonPicker extends LitElement {
fireEvent(this, "change");
}, 0);
}
private _valueRenderer = (itemId: string) => {
const item = this._addons!.find((addon) => addon.id === itemId);
return html`${item?.icon
? html`<img
slot="start"
alt=${item.primary ?? "Unknown"}
.src=${item.icon}
/>`
: nothing}<span slot="headline">${item?.primary || "Unknown"}</span>`;
};
}
declare global {

View File

@@ -0,0 +1,342 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import Fuse from "fuse.js";
import memoizeOne from "memoize-one";
import { computeEntityNameList } from "../common/entity/compute_entity_name_display";
import { computeRTL } from "../common/util/compute_rtl";
import type { LocalizeFunc } from "../common/translations/localize";
import {
multiTermSortedSearch,
type FuseWeightedKey,
} from "../resources/fuseMultiTerm";
import {
AREA_CONTROLS_BUTTONS,
getAreaControlEntities,
type AreaControlDomain,
} from "../data/area/area_controls";
import type { HomeAssistant } from "../types";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
import "./ha-combo-box-item";
import "./ha-domain-icon";
import "./ha-generic-picker";
import "./ha-state-icon";
export interface AreaControlPickerItem extends PickerComboBoxItem {
type?: "domain" | "entity";
stateObj?: HassEntity;
domain?: string;
deviceClass?: string;
}
const AREA_CONTROL_DOMAINS: readonly AreaControlDomain[] = [
"light",
"fan",
"switch",
"cover-shutter",
"cover-blind",
"cover-curtain",
"cover-shade",
"cover-awning",
"cover-garage",
"cover-gate",
"cover-door",
"cover-window",
"cover-damper",
] as const;
@customElement("ha-area-controls-picker")
export class HaAreaControlsPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "area-id" }) public areaId!: string;
@property({ type: Array, attribute: "exclude-entities" })
public excludeEntities?: string[];
@property() public value?: string;
@property({ type: Array, attribute: "exclude-values" })
public excludeValues?: string[];
@property() public label?: string;
@property() public placeholder?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
private _domainSearchKeys: FuseWeightedKey[] = [
{
name: "primary",
weight: 10,
},
];
private _entitySearchKeys: FuseWeightedKey[] = [
{
name: "primary",
weight: 10,
},
{
name: "secondary",
weight: 5,
},
{
name: "id",
weight: 3,
},
];
private _createFuseIndex = (
items: AreaControlPickerItem[],
keys: FuseWeightedKey[]
) => Fuse.createIndex(keys, items);
private _domainFuseIndex = memoizeOne((items: AreaControlPickerItem[]) =>
this._createFuseIndex(items, this._domainSearchKeys)
);
private _entityFuseIndex = memoizeOne((items: AreaControlPickerItem[]) =>
this._createFuseIndex(items, this._entitySearchKeys)
);
private _getItems = memoizeOne(
(
areaId: string,
excludeEntities: string[] | undefined,
currentValue: string | undefined,
excludeValues: string[] | undefined,
localize: LocalizeFunc,
_entities: HomeAssistant["entities"],
_devices: HomeAssistant["devices"],
_areas: HomeAssistant["areas"]
): ((
searchString?: string,
section?: string
) => (AreaControlPickerItem | string)[]) =>
(searchString?: string, section?: string) => {
if (!this.hass) {
return [];
}
const isSelected = (id: string): boolean =>
currentValue === id ||
(excludeValues !== undefined && excludeValues.includes(id));
const controlEntities = getAreaControlEntities(
AREA_CONTROL_DOMAINS as unknown as AreaControlDomain[],
areaId,
excludeEntities,
this.hass
);
const items: (AreaControlPickerItem | string)[] = [];
let domainItems: AreaControlPickerItem[] = [];
let entityItems: AreaControlPickerItem[] = [];
if (!section || section === "domain") {
const supportedControls = (
Object.keys(controlEntities) as (keyof typeof controlEntities)[]
).filter((control) => controlEntities[control].length > 0);
supportedControls.forEach((control) => {
if (isSelected(control)) {
return;
}
const label = localize(
`ui.panel.lovelace.editor.features.types.area-controls.controls_options.${control}`
);
const button = AREA_CONTROLS_BUTTONS[control];
const deviceClass = button.filter.device_class
? Array.isArray(button.filter.device_class)
? button.filter.device_class[0]
: button.filter.device_class
: undefined;
domainItems.push({
type: "domain",
id: control,
primary: label,
domain: button.filter.domain,
deviceClass,
});
});
if (searchString) {
const fuseIndex = this._domainFuseIndex(domainItems);
domainItems = multiTermSortedSearch(
domainItems,
searchString,
this._domainSearchKeys,
(item) => item.id,
fuseIndex
);
}
}
if (!section || section === "entity") {
const allEntityIds = Object.values(controlEntities).flat();
const uniqueEntityIds = Array.from(new Set(allEntityIds));
const isRTL = computeRTL(this.hass);
uniqueEntityIds.forEach((entityId) => {
if (isSelected(entityId)) {
return;
}
const stateObj = this.hass!.states[entityId];
if (!stateObj) {
return;
}
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass!.entities,
this.hass!.devices,
this.hass!.areas,
this.hass!.floors
);
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
entityItems.push({
type: "entity",
id: entityId,
primary,
secondary,
stateObj,
});
});
if (searchString) {
const fuseIndex = this._entityFuseIndex(entityItems);
entityItems = multiTermSortedSearch(
entityItems,
searchString,
this._entitySearchKeys,
(item) => item.id,
fuseIndex
);
}
}
// Only add section headers if there are items in that section
if (!section) {
if (domainItems.length > 0) {
items.push(
localize(
"ui.panel.lovelace.editor.features.types.area-controls.sections.domain"
)
);
items.push(...domainItems);
}
if (entityItems.length > 0) {
items.push(
localize(
"ui.panel.lovelace.editor.features.types.area-controls.sections.entity"
)
);
items.push(...entityItems);
}
} else {
items.push(...domainItems, ...entityItems);
}
return items;
}
);
private _rowRenderer = (item: AreaControlPickerItem) => html`
<ha-combo-box-item type="button" compact>
${item.type === "entity" && item.stateObj
? html`<ha-state-icon
slot="start"
.hass=${this.hass}
.stateObj=${item.stateObj}
></ha-state-icon>`
: item.domain
? html`<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${item.domain}
.deviceClass=${item.deviceClass}
></ha-domain-icon>`
: nothing}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.type === "entity" && item.stateObj
? html`<span slot="supporting-text" class="code">
${item.stateObj.entity_id}
</span>`
: nothing}
</ha-combo-box-item>
`;
protected render() {
if (!this.hass) {
return nothing;
}
return html`
<ha-generic-picker
.hass=${this.hass}
.value=${this.value || ""}
.disabled=${this.disabled}
.required=${this.required}
.label=${this.label}
.placeholder=${this.placeholder}
.helper=${this.helper}
.addButtonLabel=${this.addButtonLabel}
.getItems=${this._getItems(
this.areaId,
this.excludeEntities,
this.value,
this.excludeValues,
this.hass.localize,
this.hass.entities,
this.hass.devices,
this.hass.areas
)}
.rowRenderer=${this._rowRenderer as any}
.sections=${[
{
id: "domain",
label: this.hass.localize(
"ui.panel.lovelace.editor.features.types.area-controls.sections.domain"
),
},
{
id: "entity",
label: this.hass.localize(
"ui.panel.lovelace.editor.features.types.area-controls.sections.entity"
),
},
]}
></ha-generic-picker>
`;
}
static styles = css`
.code {
font-family: var(--ha-font-family-code);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-area-controls-picker": HaAreaControlsPicker;
}
}

View File

@@ -6,16 +6,10 @@ import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeAreaName } from "../common/entity/compute_area_name";
import { computeDomain } from "../common/entity/compute_domain";
import { computeFloorName } from "../common/entity/compute_floor_name";
import { getAreaContext } from "../common/entity/context/get_area_context";
import { createAreaRegistryEntry } from "../data/area_registry";
import type {
DeviceEntityDisplayLookup,
DeviceRegistryEntry,
} from "../data/device/device_registry";
import { getDeviceEntityDisplayLookup } from "../data/device/device_registry";
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
import { areaComboBoxKeys, getAreas } from "../data/area/area_picker";
import { createAreaRegistryEntry } from "../data/area/area_registry";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
import type { HomeAssistant, ValueChangedEvent } from "../types";
@@ -30,12 +24,6 @@ import "./ha-svg-icon";
const ADD_NEW_ID = "___ADD_NEW___";
const SEARCH_KEYS = [
{ name: "search_labels.areaName", weight: 10 },
{ name: "search_labels.aliases", weight: 8 },
{ name: "search_labels.floorName", weight: 6 },
{ name: "search_labels.id", weight: 3 },
];
@customElement("ha-area-picker")
export class HaAreaPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -102,6 +90,8 @@ export class HaAreaPicker extends LitElement {
await this._picker?.open();
}
private _getAreasMemoized = memoizeOne(getAreas);
// Recompute value renderer when the areas change
private _computeValueRenderer = memoizeOne(
(_haAreas: HomeAssistant["areas"]): PickerValueRenderer =>
@@ -137,183 +127,13 @@ export class HaAreaPicker extends LitElement {
}
);
private _getAreas = memoizeOne(
(
haAreas: HomeAssistant["areas"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"],
entityFilter: this["entityFilter"],
excludeAreas: this["excludeAreas"]
): PickerComboBoxItem[] => {
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined;
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
const areas = Object.values(haAreas);
const devices = Object.values(haDevices);
const entities = Object.values(haEntities);
if (
includeDomains ||
excludeDomains ||
includeDeviceClasses ||
deviceFilter ||
entityFilter
) {
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
inputDevices = devices;
inputEntities = entities.filter((entity) => entity.area_id);
if (includeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
}
if (excludeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
}
if (includeDeviceClasses) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = this.hass.states[entity.entity_id];
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
}
if (deviceFilter) {
inputDevices = inputDevices!.filter((device) =>
deviceFilter!(device)
);
}
if (entityFilter) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter(stateObj);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter!(stateObj);
});
}
}
let outputAreas = areas;
let areaIds: string[] | undefined;
if (inputDevices) {
areaIds = inputDevices
.filter((device) => device.area_id)
.map((device) => device.area_id!);
}
if (inputEntities) {
areaIds = (areaIds ?? []).concat(
inputEntities
.filter((entity) => entity.area_id)
.map((entity) => entity.area_id!)
);
}
if (areaIds) {
outputAreas = outputAreas.filter((area) =>
areaIds!.includes(area.area_id)
);
}
if (excludeAreas) {
outputAreas = outputAreas.filter(
(area) => !excludeAreas!.includes(area.area_id)
);
}
const items = outputAreas.map<PickerComboBoxItem>((area) => {
const { floor } = getAreaContext(area, this.hass.floors);
const floorName = floor ? computeFloorName(floor) : undefined;
const areaName = computeAreaName(area);
return {
id: area.area_id,
primary: areaName || area.area_id,
secondary: floorName,
icon: area.icon || undefined,
icon_path: area.icon ? undefined : mdiTextureBox,
search_labels: {
areaName: areaName || null,
floorName: floorName || null,
id: area.area_id,
aliases: area.aliases.join(" "),
},
};
});
return items;
}
);
private _getItems = () =>
this._getAreas(
this._getAreasMemoized(
this.hass.areas,
this.hass.floors,
this.hass.devices,
this.hass.entities,
this.hass.states,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
@@ -363,28 +183,38 @@ export class HaAreaPicker extends LitElement {
};
protected render(): TemplateResult {
const placeholder =
this.placeholder ?? this.hass.localize("ui.components.area-picker.area");
const baseLabel =
this.label ?? this.hass.localize("ui.components.area-picker.area");
const valueRenderer = this._computeValueRenderer(this.hass.areas);
// Only show label if there's no floor
let label: string | undefined = baseLabel;
if (this.value && baseLabel) {
const area = this.hass.areas[this.value];
if (area) {
const { floor } = getAreaContext(area, this.hass.floors);
if (floor) {
label = undefined;
}
}
}
return html`
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
.label=${this.label}
.label=${label}
.helper=${this.helper}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
.disabled=${this.disabled}
.required=${this.required}
.placeholder=${placeholder}
.value=${this.value}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${valueRenderer}
.addButtonLabel=${this.addButtonLabel}
.searchKeys=${SEARCH_KEYS}
.searchKeys=${areaComboBoxKeys}
.unknownItemText=${this.hass.localize(
"ui.components.area-picker.unknown"
)}

View File

@@ -52,7 +52,9 @@ export class HaAutomationRow extends LitElement {
<slot name="leading-icon"></slot>
</div>
<slot class="header" name="header"></slot>
<slot name="icons"></slot>
<div class="icons">
<slot name="icons"></slot>
</div>
</div>
`;
}
@@ -118,12 +120,11 @@ export class HaAutomationRow extends LitElement {
}
.row {
display: flex;
padding: var(--ha-space-0) var(--ha-space-2);
padding: 0 var(--ha-space-3);
min-height: 48px;
align-items: center;
align-items: flex-start;
cursor: pointer;
overflow: hidden;
font-weight: var(--ha-font-weight-medium);
outline: none;
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
}
@@ -140,11 +141,15 @@ export class HaAutomationRow extends LitElement {
background-color: var(--ha-color-fill-neutral-loud-resting);
border-radius: var(--ha-border-radius-md);
padding: var(--ha-space-1);
margin-top: 10px;
display: flex;
justify-content: center;
align-items: center;
transform: rotate(45deg);
}
.leading-icon-wrapper {
padding-top: var(--ha-space-3);
}
::slotted([slot="leading-icon"]) {
color: var(--ha-color-on-neutral-quiet);
}
@@ -169,8 +174,14 @@ export class HaAutomationRow extends LitElement {
}
::slotted([slot="header"]) {
flex: 1;
min-width: 0;
overflow-wrap: anywhere;
margin: var(--ha-space-0) var(--ha-space-3);
margin: 0 var(--ha-space-3);
}
.icons {
display: flex;
align-items: center;
flex-shrink: 0;
}
:host([sort-selected]) .row {
outline: solid;

View File

@@ -208,7 +208,8 @@ export class HaBaseTimeInput extends LitElement {
? html`<ha-textfield
id="sec"
type="number"
inputmode="numeric"
inputmode="decimal"
step="any"
.value=${this._formatValue(this.seconds)}
.label=${this.secLabel}
@change=${this._valueChanged}
@@ -217,7 +218,6 @@ export class HaBaseTimeInput extends LitElement {
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
maxlength="2"
max="59"
min="0"
.disabled=${this.disabled}
@@ -311,7 +311,8 @@ export class HaBaseTimeInput extends LitElement {
* Format time fragments
*/
private _formatValue(value: number, padding = 2) {
return value.toString().padStart(padding, "0");
const str = value.toString();
return str.includes(".") ? str : str.padStart(padding, "0");
}
/**

View File

@@ -241,7 +241,7 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
}
slot[name="footer"] {
display: block;
padding: var(--ha-space-0);
padding: 0;
}
::slotted([slot="footer"]) {
display: flex;

View File

@@ -46,14 +46,17 @@ export class HaCard extends LitElement {
line-height: var(--ha-line-height-expanded);
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4);
display: block;
margin-block-start: var(--ha-space-0);
margin-block-end: var(--ha-space-0);
margin-block-start: 0;
margin-block-end: 0;
font-weight: var(--ha-font-weight-normal);
}
:host ::slotted(.card-content:not(:first-child)),
:host
::slotted(
.card-content:not(:nth-child(1 of .card-content, .card-header))
),
slot:not(:first-child)::slotted(.card-content) {
padding-top: var(--ha-space-0);
padding-top: 0;
margin-top: calc(var(--ha-space-2) * -1);
}

View File

@@ -255,6 +255,7 @@ export class HaCodeEditor extends ReactiveElement {
...this._loadedCodeMirror.tabKeyBindings,
saveKeyBinding,
]),
this._loadedCodeMirror.search({ top: true }),
this._loadedCodeMirror.langCompartment.of(this._mode),
this._loadedCodeMirror.haTheme,
this._loadedCodeMirror.haSyntaxHighlighting,

View File

@@ -1,25 +1,24 @@
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import type { LocalizeKeys } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-md-divider";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
import type { PickerValueRenderer } from "./ha-picker-field";
@customElement("ha-color-picker")
export class HaColorPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property() public helper?: string;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value?: string;
@property({ type: String, attribute: "default_color" })
@@ -33,137 +32,208 @@ export class HaColorPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-select") private _select?: HaSelect;
connectedCallback(): void {
super.connectedCallback();
// Refresh layout options when the field is connected to the DOM to ensure current value displayed
this._select?.layoutOptions();
}
private _valueSelected(ev) {
ev.stopPropagation();
if (!this.isConnected) return;
const value = ev.target.value;
this.value = value === this.defaultColor ? undefined : value;
fireEvent(this, "value-changed", {
value: this.value,
});
}
@property({ type: Boolean }) public required = false;
render() {
const value = this.value || this.defaultColor || "";
const isCustom = !(
THEME_COLORS.has(value) ||
value === "none" ||
value === "state"
);
const effectiveValue = this.value ?? this.defaultColor ?? "";
return html`
<ha-select
.icon=${Boolean(value)}
.label=${this.label}
.value=${value}
.helper=${this.helper}
<ha-generic-picker
.hass=${this.hass}
.disabled=${this.disabled}
@closed=${stopPropagation}
@selected=${this._valueSelected}
fixedMenuPosition
naturalMenuWidth
.clearable=${!this.defaultColor}
>
${value
? html`
<span slot="icon">
${value === "none"
? html`
<ha-svg-icon path=${mdiInvertColorsOff}></ha-svg-icon>
`
: value === "state"
? html`<ha-svg-icon path=${mdiPalette}></ha-svg-icon>`
: this._renderColorCircle(value || "grey")}
</span>
`
: nothing}
${this.includeNone
? html`
<ha-list-item value="none" graphic="icon">
${this.hass.localize("ui.components.color-picker.none")}
${this.defaultColor === "none"
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<ha-svg-icon
slot="graphic"
path=${mdiInvertColorsOff}
></ha-svg-icon>
</ha-list-item>
`
: nothing}
${this.includeState
? html`
<ha-list-item value="state" graphic="icon">
${this.hass.localize("ui.components.color-picker.state")}
${this.defaultColor === "state"
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<ha-svg-icon slot="graphic" path=${mdiPalette}></ha-svg-icon>
</ha-list-item>
`
: nothing}
${this.includeState || this.includeNone
? html`<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
: nothing}
${Array.from(THEME_COLORS).map(
(color) => html`
<ha-list-item .value=${color} graphic="icon">
${this.hass.localize(
`ui.components.color-picker.colors.${color}` as LocalizeKeys
) || color}
${this.defaultColor === color
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<span slot="graphic">${this._renderColorCircle(color)}</span>
</ha-list-item>
`
.required=${this.required}
.hideClearIcon=${!this.value && !!this.defaultColor}
.label=${this.label}
.helper=${this.helper}
.value=${effectiveValue}
.getItems=${this._getItems}
.rowRenderer=${this._rowRenderer}
.valueRenderer=${this._valueRenderer}
@value-changed=${this._valueChanged}
.notFoundLabel=${this.hass.localize(
"ui.components.color-picker.no_colors_found"
)}
${isCustom
? html`
<ha-list-item .value=${value} graphic="icon">
${value}
<span slot="graphic">${this._renderColorCircle(value)}</span>
</ha-list-item>
`
: nothing}
</ha-select>
.getAdditionalItems=${this._getAdditionalItems}
>
</ha-generic-picker>
`;
}
private _getAdditionalItems = (
searchString?: string
): PickerComboBoxItem[] => {
if (!searchString || searchString.trim() === "") {
return [];
}
const colors = this._getColors(
this.includeNone,
this.includeState,
this.defaultColor,
this.value
);
const exactMatch = colors.find((color) => color.id === searchString);
if (exactMatch) {
return [];
}
return [
{
id: searchString,
primary: this.hass.localize("ui.components.color-picker.custom_color"),
secondary: searchString,
},
];
};
private _getItems = () =>
this._getColors(
this.includeNone,
this.includeState,
this.defaultColor,
this.value
);
private _getColors = memoizeOne(
(
includeNone: boolean,
includeState: boolean,
defaultColor: string | undefined,
currentValue: string | undefined
): PickerComboBoxItem[] => {
const items: PickerComboBoxItem[] = [];
const defaultSuffix = this.hass.localize(
"ui.components.color-picker.default"
);
const addDefaultSuffix = (label: string, isDefault: boolean) =>
isDefault && defaultSuffix ? `${label} (${defaultSuffix})` : label;
if (includeNone) {
const noneLabel =
this.hass.localize("ui.components.color-picker.none") || "None";
items.push({
id: "none",
primary: addDefaultSuffix(noneLabel, defaultColor === "none"),
icon_path: mdiInvertColorsOff,
});
}
if (includeState) {
const stateLabel =
this.hass.localize("ui.components.color-picker.state") || "State";
items.push({
id: "state",
primary: addDefaultSuffix(stateLabel, defaultColor === "state"),
icon_path: mdiPalette,
});
}
Array.from(THEME_COLORS).forEach((color) => {
const themeLabel =
this.hass.localize(
`ui.components.color-picker.colors.${color}` as LocalizeKeys
) || color;
items.push({
id: color,
primary: addDefaultSuffix(themeLabel, defaultColor === color),
});
});
const isSpecial =
currentValue === "none" ||
currentValue === "state" ||
THEME_COLORS.has(currentValue || "");
const hasValue = currentValue && currentValue.length > 0;
if (hasValue && !isSpecial) {
items.push({
id: currentValue!,
primary: currentValue!,
});
}
return items;
}
);
private _rowRenderer: (
item: PickerComboBoxItem,
index?: number
) => ReturnType<typeof html> = (item) => html`
<ha-combo-box-item type="button" compact>
${item.id === "none"
? html`<ha-svg-icon
slot="start"
.path=${mdiInvertColorsOff}
></ha-svg-icon>`
: item.id === "state"
? html`<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>`
: html`<span slot="start">
${this._renderColorCircle(item.id)}
</span>`}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
</ha-combo-box-item>
`;
private _valueRenderer: PickerValueRenderer = (value: string) => {
if (value === "none") {
return html`
<ha-svg-icon slot="start" .path=${mdiInvertColorsOff}></ha-svg-icon>
<span slot="headline">
${this.hass.localize("ui.components.color-picker.none")}
</span>
`;
}
if (value === "state") {
return html`
<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>
<span slot="headline">
${this.hass.localize("ui.components.color-picker.state")}
</span>
`;
}
return html`
<span slot="start">${this._renderColorCircle(value)}</span>
<span slot="headline">
${this.hass.localize(
`ui.components.color-picker.colors.${value}` as LocalizeKeys
) || value}
</span>
`;
};
private _renderColorCircle(color: string) {
return html`
<span
class="circle-color"
style=${styleMap({
"--circle-color": computeCssColor(color),
display: "block",
"background-color": "var(--circle-color, var(--divider-color))",
border: "1px solid var(--outline-color)",
"border-radius": "var(--ha-border-radius-pill)",
width: "20px",
height: "20px",
"box-sizing": "border-box",
})}
></span>
`;
}
static styles = css`
.circle-color {
display: block;
background-color: var(--circle-color, var(--divider-color));
border: 1px solid var(--outline-color);
border-radius: var(--ha-border-radius-pill);
width: 20px;
height: 20px;
box-sizing: border-box;
}
ha-select {
width: 100%;
}
`;
private _valueChanged(ev: CustomEvent<{ value?: string }>) {
ev.stopPropagation();
const selected = ev.detail.value;
const normalized =
selected && selected === this.defaultColor
? undefined
: (selected ?? undefined);
this.value = normalized;
fireEvent(this, "value-changed", { value: this.value });
}
}
declare global {

View File

@@ -20,6 +20,17 @@ export class HaComboBoxItem extends HaMdListItem {
[slot="start"] {
--state-icon-color: var(--secondary-text-color);
}
[slot="overline"] {
/* mimicing a floating label of mdc-select */
line-height: 1.15rem;
font-size: calc(var(--mdc-typography-subtitle1-font-size, 1rem) * 0.75);
font-weight: var(--mdc-typography-subtitle1-font-weight, 400);
font-family: var(
--mdc-typography-subtitle1-font-family,
var(--mdc-typography-font-family)
);
color: var(--mdc-select-label-ink-color, rgba(0, 0, 0, 0.6));
}
[slot="headline"] {
line-height: var(--ha-line-height-normal);
font-size: var(--ha-font-size-m);

View File

@@ -1,24 +0,0 @@
import type { PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { HaTextField } from "./ha-textfield";
@customElement("ha-combo-box-textfield")
export class HaComboBoxTextField extends HaTextField {
@property({ type: Boolean, attribute: "force-blank-value" })
public forceBlankValue = false;
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("value") || changedProps.has("forceBlankValue")) {
if (this.forceBlankValue && this.value) {
this.value = "";
}
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-combo-box-textfield": HaComboBoxTextField;
}
}

View File

@@ -1,433 +0,0 @@
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { comboBoxRenderer } from "@vaadin/combo-box/lit";
import "@vaadin/combo-box/theme/material/vaadin-combo-box-light";
import type {
ComboBoxDataProvider,
ComboBoxLight,
ComboBoxLightFilterChangedEvent,
ComboBoxLightOpenedChangedEvent,
ComboBoxLightValueChangedEvent,
} from "@vaadin/combo-box/vaadin-combo-box-light";
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant } from "../types";
import "./ha-combo-box-item";
import "./ha-combo-box-textfield";
import "./ha-icon-button";
import "./ha-input-helper-text";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
registerStyles(
"vaadin-combo-box-item",
css`
:host {
padding: 0 !important;
}
:host([focused]:not([disabled])) {
background-color: rgba(var(--rgb-primary-text-color, 0, 0, 0), 0.12);
}
:host([selected]:not([disabled])) {
background-color: transparent;
color: var(--mdc-theme-primary);
--mdc-ripple-color: var(--mdc-theme-primary);
--mdc-theme-text-primary-on-background: var(--mdc-theme-primary);
}
:host([selected]:not([disabled])):before {
background-color: var(--mdc-theme-primary);
opacity: 0.12;
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
:host([selected][focused]:not([disabled])):before {
opacity: 0.24;
}
:host(:hover:not([disabled])) {
background-color: transparent;
}
[part="content"] {
width: 100%;
}
[part="checkmark"] {
display: none;
}
`
);
@customElement("ha-combo-box")
export class HaComboBox extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public placeholder?: string;
@property({ attribute: false }) public validationMessage?: string;
@property() public helper?: string;
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public invalid = false;
@property({ type: Boolean }) public icon = false;
@property({ attribute: false }) public items?: any[];
@property({ attribute: false }) public filteredItems?: any[];
@property({ attribute: false })
public dataProvider?: ComboBoxDataProvider<any>;
@property({ attribute: "allow-custom-value", type: Boolean })
public allowCustomValue = false;
@property({ attribute: "item-value-path" }) public itemValuePath = "value";
@property({ attribute: "item-label-path" }) public itemLabelPath = "label";
@property({ attribute: "item-id-path" }) public itemIdPath?: string;
@property({ attribute: false }) public renderer?: ComboBoxLitRenderer<any>;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, reflect: true }) public opened = false;
@property({ type: Boolean, attribute: "hide-clear-icon" })
public hideClearIcon = false;
@property({ type: Boolean, attribute: "clear-initial-value" })
public clearInitialValue = false;
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
@query("ha-combo-box-textfield", true) private _inputElement!: HaTextField;
@state({ type: Boolean }) private _forceBlankValue = false;
private _overlayMutationObserver?: MutationObserver;
private _bodyMutationObserver?: MutationObserver;
public async open() {
await this.updateComplete;
this._comboBox?.open();
}
public async focus() {
await this.updateComplete;
await this._inputElement?.updateComplete;
this._inputElement?.focus();
}
public disconnectedCallback() {
super.disconnectedCallback();
if (this._overlayMutationObserver) {
this._overlayMutationObserver.disconnect();
this._overlayMutationObserver = undefined;
}
if (this._bodyMutationObserver) {
this._bodyMutationObserver.disconnect();
this._bodyMutationObserver = undefined;
}
}
public get selectedItem() {
return this._comboBox.selectedItem;
}
public setInputValue(value: string) {
this._comboBox.value = value;
}
public setTextFieldValue(value: string) {
this._inputElement.value = value;
}
protected render(): TemplateResult {
return html`
<!-- @ts-ignore Tag definition is not included in theme folder -->
<vaadin-combo-box-light
.itemValuePath=${this.itemValuePath}
.itemIdPath=${this.itemIdPath}
.itemLabelPath=${this.itemLabelPath}
.items=${this.items}
.value=${this.value || ""}
.filteredItems=${this.filteredItems}
.dataProvider=${this.dataProvider}
.allowCustomValue=${this.allowCustomValue}
.disabled=${this.disabled}
.required=${this.required}
${comboBoxRenderer(this.renderer || this._defaultRowRenderer)}
@opened-changed=${this._openedChanged}
@filter-changed=${this._filterChanged}
@value-changed=${this._valueChanged}
attr-for-value="value"
>
<ha-combo-box-textfield
label=${ifDefined(this.label)}
placeholder=${ifDefined(this.placeholder)}
?disabled=${this.disabled}
?required=${this.required}
validationMessage=${ifDefined(this.validationMessage)}
.errorMessage=${this.errorMessage}
class="input"
autocapitalize="none"
autocomplete="off"
.autocorrect=${false}
input-spellcheck="false"
.suffix=${html`<div
style="width: 28px;"
role="none presentation"
></div>`}
.icon=${this.icon}
.invalid=${this.invalid}
.forceBlankValue=${this._forceBlankValue}
>
<slot name="icon" slot="leadingIcon"></slot>
</ha-combo-box-textfield>
${this.value && !this.hideClearIcon
? html`<ha-svg-icon
role="button"
tabindex="-1"
aria-label=${ifDefined(this.hass?.localize("ui.common.clear"))}
class=${`clear-button ${this.label ? "" : "no-label"}`}
.path=${mdiClose}
?disabled=${this.disabled}
@click=${this._clearValue}
></ha-svg-icon>`
: ""}
<ha-svg-icon
role="button"
tabindex="-1"
aria-label=${ifDefined(this.label)}
aria-expanded=${this.opened ? "true" : "false"}
class=${`toggle-button ${this.label ? "" : "no-label"}`}
.path=${this.opened ? mdiMenuUp : mdiMenuDown}
?disabled=${this.disabled}
@click=${this._toggleOpen}
></ha-svg-icon>
</vaadin-combo-box-light>
${this._renderHelper()}
`;
}
private _renderHelper() {
return this.helper
? html`<ha-input-helper-text .disabled=${this.disabled}
>${this.helper}</ha-input-helper-text
>`
: "";
}
private _defaultRowRenderer: ComboBoxLitRenderer<
string | Record<string, any>
> = (item) => html`
<ha-combo-box-item type="button">
${this.itemLabelPath ? item[this.itemLabelPath] : item}
</ha-combo-box-item>
`;
private _clearValue(ev: Event) {
ev.stopPropagation();
fireEvent(this, "value-changed", { value: undefined });
}
private _toggleOpen(ev: Event) {
if (this.opened) {
this._comboBox?.close();
ev.stopPropagation();
} else {
this._comboBox?.inputElement.focus();
}
}
private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
ev.stopPropagation();
const opened = ev.detail.value;
// delay this so we can handle click event for toggle button before setting _opened
setTimeout(() => {
this.opened = opened;
fireEvent(this, "opened-changed", { value: ev.detail.value });
}, 0);
if (this.clearInitialValue) {
this.setTextFieldValue("");
if (opened) {
// Wait 100ms to be sure vaddin-combo-box-light already tried to set the value
setTimeout(() => {
this._forceBlankValue = false;
}, 100);
} else {
this._forceBlankValue = true;
}
}
if (opened) {
const overlay = document.querySelector<HTMLElement>(
"vaadin-combo-box-overlay"
);
if (overlay) {
this._removeInert(overlay);
}
this._observeBody();
} else {
this._bodyMutationObserver?.disconnect();
this._bodyMutationObserver = undefined;
}
}
private _observeBody() {
if ("MutationObserver" in window && !this._bodyMutationObserver) {
this._bodyMutationObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") {
this._removeInert(node as HTMLElement);
}
});
mutation.removedNodes.forEach((node) => {
if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") {
this._overlayMutationObserver?.disconnect();
this._overlayMutationObserver = undefined;
}
});
});
});
this._bodyMutationObserver.observe(document.body, {
childList: true,
});
}
}
private _removeInert(overlay: HTMLElement) {
if (overlay.inert) {
overlay.inert = false;
this._overlayMutationObserver?.disconnect();
this._overlayMutationObserver = undefined;
return;
}
if ("MutationObserver" in window && !this._overlayMutationObserver) {
this._overlayMutationObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === "inert") {
const target = mutation.target as HTMLElement;
if (target.inert) {
this._overlayMutationObserver?.disconnect();
this._overlayMutationObserver = undefined;
target.inert = false;
}
}
});
});
this._overlayMutationObserver.observe(overlay, {
attributes: true,
});
}
}
private _filterChanged(ev: ComboBoxLightFilterChangedEvent) {
ev.stopPropagation();
fireEvent(this, "filter-changed", { value: ev.detail.value });
}
private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
ev.stopPropagation();
if (!this.allowCustomValue) {
// @ts-ignore
this._comboBox._closeOnBlurIsPrevented = true;
}
if (!this.opened) {
return;
}
const newValue = ev.detail.value;
if (newValue !== this.value) {
fireEvent(this, "value-changed", { value: newValue || undefined });
}
}
static styles = css`
:host {
display: block;
width: 100%;
}
vaadin-combo-box-light {
position: relative;
}
ha-combo-box-textfield {
width: 100%;
}
ha-combo-box-textfield > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
ha-svg-icon {
color: var(--input-dropdown-icon-color);
position: absolute;
cursor: pointer;
}
.toggle-button {
right: 12px;
top: -10px;
inset-inline-start: initial;
inset-inline-end: 12px;
direction: var(--direction);
}
:host([opened]) .toggle-button {
color: var(--primary-color);
}
.toggle-button[disabled],
.clear-button[disabled] {
color: var(--disabled-text-color);
pointer-events: none;
}
.toggle-button.no-label {
top: -3px;
}
.clear-button {
--mdc-icon-size: 20px;
top: -7px;
right: 36px;
inset-inline-start: initial;
inset-inline-end: 36px;
direction: var(--direction);
}
.clear-button.no-label {
top: 0;
}
ha-input-helper-text {
margin-top: 4px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-combo-box": HaComboBox;
}
}
declare global {
interface HASSDomEvents {
"filter-changed": { value: string };
"opened-changed": { value: boolean };
}
}

View File

@@ -1,20 +1,22 @@
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { ConfigEntry } from "../data/config_entries";
import { getConfigEntries } from "../data/config_entries";
import { domainToName } from "../data/integration";
import type { ValueChangedEvent, HomeAssistant } from "../types";
import { brandsUrl } from "../util/brands-url";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-combo-box-item";
import "./ha-domain-icon";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
export interface ConfigEntryExtended extends ConfigEntry {
localized_domain_name?: string;
}
const SEARCH_KEYS = [
{ name: "primary", weight: 10 },
{ name: "secondary", weight: 8 },
{ name: "icon", weight: 5 },
];
@customElement("ha-config-entry-picker")
class HaConfigEntryPicker extends LitElement {
@@ -28,119 +30,106 @@ class HaConfigEntryPicker extends LitElement {
@property() public helper?: string;
@state() private _configEntries?: ConfigEntryExtended[];
@state() private _configEntries?: PickerComboBoxItem[];
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@query("ha-combo-box") private _comboBox!: HaComboBox;
@query("ha-generic-picker") private _picker!: HaGenericPicker;
public open() {
this._comboBox?.open();
this._picker?.open();
}
public focus() {
this._comboBox?.focus();
this._picker?.focus();
}
protected firstUpdated() {
this._getConfigEntries();
}
private _rowRenderer: ComboBoxLitRenderer<ConfigEntryExtended> = (
item
) => html`
<ha-combo-box-item type="button">
<span slot="headline">
${item.title ||
this.hass.localize(
"ui.panel.config.integrations.config_entry.unnamed_entry"
)}
</span>
<span slot="supporting-text">${item.localized_domain_name}</span>
<img
alt=""
slot="start"
src=${brandsUrl({
domain: item.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</ha-combo-box-item>
`;
protected render() {
if (!this._configEntries) {
return nothing;
}
return html`
<ha-combo-box
<ha-generic-picker
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.config-entry-picker.config_entry")
: this.label}
.value=${this._value}
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}
.helper=${this.helper}
.renderer=${this._rowRenderer}
.items=${this._configEntries}
item-value-path="entry_id"
item-id-path="entry_id"
item-label-path="title"
.rowRenderer=${this._rowRenderer}
.getItems=${this._getItems}
.searchKeys=${SEARCH_KEYS}
.valueRenderer=${this._valueRenderer}
@value-changed=${this._valueChanged}
></ha-combo-box>
></ha-generic-picker>
`;
}
private _onImageLoad(ev) {
ev.target.style.visibility = "initial";
}
private _onImageError(ev) {
ev.target.style.visibility = "hidden";
}
private _rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
<ha-combo-box-item type="button">
<span slot="headline">${item.primary}</span>
<span slot="supporting-text">${item.secondary}</span>
<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${item.icon!}
brand-fallback
></ha-domain-icon>
</ha-combo-box-item>
`;
private async _getConfigEntries() {
getConfigEntries(this.hass, {
type: ["device", "hub", "service"],
domain: this.integration,
}).then((configEntries) => {
this._configEntries = configEntries
.map(
(entry: ConfigEntry): ConfigEntryExtended => ({
...entry,
localized_domain_name: domainToName(
this.hass.localize,
entry.domain
this._configEntries = configEntries.map((entry: ConfigEntry) => {
const domainName = domainToName(this.hass.localize, entry.domain);
return {
id: entry.entry_id,
icon: entry.domain,
primary:
entry.title ||
this.hass.localize(
"ui.panel.config.integrations.config_entry.unnamed_entry"
),
})
)
.sort((conf1, conf2) =>
caseInsensitiveStringCompare(
conf1.localized_domain_name + conf1.title,
conf2.localized_domain_name + conf2.title,
this.hass.locale.language
)
);
secondary: domainName,
sorting_label: [entry.title, domainName].filter(Boolean).join("_"),
};
});
});
}
private get _value() {
return this.value || "";
}
private _valueRenderer = (itemId: string) => {
const item = this._configEntries!.find((entry) => entry.id === itemId);
return html`<span
style="display: flex; align-items: center; gap: var(--ha-space-2);"
slot="headline"
>${item?.icon
? html`<ha-domain-icon
.hass=${this.hass}
.domain=${item.icon!}
brand-fallback
></ha-domain-icon>`
: nothing}${item?.primary || "Unknown"}</span
>`;
};
private _getItems = () => this._configEntries!;
private _valueChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (newValue !== this._value) {
if (newValue !== this.value) {
this._setValue(newValue);
}
}

View File

@@ -74,6 +74,9 @@ export class HaDateRangePicker extends LitElement {
@property({ attribute: "extended-presets", type: Boolean })
public extendedPresets = false;
@property({ attribute: "vertical-opening-direction" })
public verticalOpeningDirection?: "up" | "down";
@property({ attribute: false }) public openingDirection?:
| "right"
| "left"
@@ -127,6 +130,7 @@ export class HaDateRangePicker extends LitElement {
opening-direction=${ifDefined(
this.openingDirection || this._calcedOpeningDirection
)}
opens-vertical=${ifDefined(this.verticalOpeningDirection)}
first-day=${firstWeekdayIndex(this.hass.locale)}
language=${this.hass.locale.language}
@change=${this._handleChange}

View File

@@ -101,14 +101,13 @@ export class HaDialog extends DialogBase {
}
.mdc-dialog__container {
align-items: var(--vertical-align-dialog, center);
padding: var(--dialog-container-padding, var(--ha-space-0));
padding: var(--dialog-container-padding, 0);
}
.mdc-dialog__title {
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-0)
var(--ha-space-4);
padding: var(--ha-space-4) var(--ha-space-4) 0 var(--ha-space-4);
}
.mdc-dialog__title:has(span) {
padding: var(--ha-space-3) var(--ha-space-3) var(--ha-space-0);
padding: var(--ha-space-3) var(--ha-space-3) 0;
}
.mdc-dialog__title::before {
content: unset;
@@ -136,7 +135,7 @@ export class HaDialog extends DialogBase {
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
padding: var(--dialog-surface-padding, var(--ha-space-0));
padding: var(--dialog-surface-padding, 0);
}
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
display: flex;
@@ -168,7 +167,7 @@ export class HaDialog extends DialogBase {
}
.dialog-actions {
inset-inline-start: initial !important;
inset-inline-end: var(--ha-space-0) !important;
inset-inline-end: 0 !important;
direction: var(--direction);
}
`,

View File

@@ -47,6 +47,7 @@ export class HaDomainIcon extends LitElement {
if (icn) {
return html`<ha-icon .icon=${icn}></ha-icon>`;
}
return this._renderFallback();
});

View File

@@ -1,9 +1,11 @@
import { mdiMinusThick, mdiPlusThick } from "@mdi/js";
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-base-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
import "./ha-button-toggle-group";
export interface HaDurationData {
days?: number;
@@ -13,6 +15,8 @@ export interface HaDurationData {
milliseconds?: number;
}
const FIELDS = ["milliseconds", "seconds", "minutes", "hours", "days"];
@customElement("ha-duration-input")
class HaDurationInput extends LitElement {
@property({ attribute: false }) public data?: HaDurationData;
@@ -29,41 +33,80 @@ class HaDurationInput extends LitElement {
@property({ attribute: "enable-day", type: Boolean })
public enableDay = false;
@property({ attribute: "allow-negative", type: Boolean })
public allowNegative = false;
@property({ type: Boolean }) public disabled = false;
private _toggleNegative = false;
protected render(): TemplateResult {
return html`
<ha-base-time-input
.label=${this.label}
.helper=${this.helper}
.required=${this.required}
.clearable=${!this.required && this.data !== undefined}
.autoValidate=${this.required}
.disabled=${this.disabled}
errorMessage="Required"
enable-second
.enableMillisecond=${this.enableMillisecond}
.enableDay=${this.enableDay}
format="24"
.days=${this._days}
.hours=${this._hours}
.minutes=${this._minutes}
.seconds=${this._seconds}
.milliseconds=${this._milliseconds}
@value-changed=${this._durationChanged}
no-hours-limit
day-label="dd"
hour-label="hh"
min-label="mm"
sec-label="ss"
ms-label="ms"
></ha-base-time-input>
<div class="row">
${this.allowNegative
? html`
<ha-button-toggle-group
size="small"
.buttons=${[
{ label: "+", iconPath: mdiPlusThick, value: "+" },
{ label: "-", iconPath: mdiMinusThick, value: "-" },
]}
.active=${this._negative ? "-" : "+"}
@value-changed=${this._negativeChanged}
></ha-button-toggle-group>
`
: nothing}
<ha-base-time-input
.label=${this.label}
.helper=${this.helper}
.required=${this.required}
.clearable=${!this.required && this.data !== undefined}
.autoValidate=${this.required}
.disabled=${this.disabled}
errorMessage="Required"
enable-second
.enableMillisecond=${this.enableMillisecond}
.enableDay=${this.enableDay}
format="24"
.days=${this._days}
.hours=${this._hours}
.minutes=${this._minutes}
.seconds=${this._seconds}
.milliseconds=${this._milliseconds}
@value-changed=${this._durationChanged}
no-hours-limit
day-label="dd"
hour-label="hh"
min-label="mm"
sec-label="ss"
ms-label="ms"
></ha-base-time-input>
</div>
`;
}
private get _negative() {
return (
this._toggleNegative ||
(this.data?.days
? this.data.days < 0
: this.data?.hours
? this.data.hours < 0
: this.data?.minutes
? this.data.minutes < 0
: this.data?.seconds
? this.data.seconds < 0
: this.data?.milliseconds
? this.data.milliseconds < 0
: false)
);
}
private get _days() {
return this.data?.days
? Number(this.data.days)
? this.allowNegative
? Math.abs(Number(this.data.days))
: Number(this.data.days)
: this.required || this.data
? 0
: NaN;
@@ -71,7 +114,9 @@ class HaDurationInput extends LitElement {
private get _hours() {
return this.data?.hours
? Number(this.data.hours)
? this.allowNegative
? Math.abs(Number(this.data.hours))
: Number(this.data.hours)
: this.required || this.data
? 0
: NaN;
@@ -79,7 +124,9 @@ class HaDurationInput extends LitElement {
private get _minutes() {
return this.data?.minutes
? Number(this.data.minutes)
? this.allowNegative
? Math.abs(Number(this.data.minutes))
: Number(this.data.minutes)
: this.required || this.data
? 0
: NaN;
@@ -87,7 +134,9 @@ class HaDurationInput extends LitElement {
private get _seconds() {
return this.data?.seconds
? Number(this.data.seconds)
? this.allowNegative
? Math.abs(Number(this.data.seconds))
: Number(this.data.seconds)
: this.required || this.data
? 0
: NaN;
@@ -95,7 +144,9 @@ class HaDurationInput extends LitElement {
private get _milliseconds() {
return this.data?.milliseconds
? Number(this.data.milliseconds)
? this.allowNegative
? Math.abs(Number(this.data.milliseconds))
: Number(this.data.milliseconds)
: this.required || this.data
? 0
: NaN;
@@ -113,6 +164,14 @@ class HaDurationInput extends LitElement {
if ("days" in value) value.days ||= 0;
if ("milliseconds" in value) value.milliseconds ||= 0;
if (this.allowNegative) {
FIELDS.forEach((t) => {
if (value[t]) {
value[t] = Math.abs(value[t]);
}
});
}
if (!this.enableMillisecond && !value.milliseconds) {
// @ts-ignore
delete value.milliseconds;
@@ -135,12 +194,47 @@ class HaDurationInput extends LitElement {
value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
value.hours %= 24;
}
if (this._negative) {
FIELDS.forEach((t) => {
if (value[t]) {
value[t] = -Math.abs(value[t]);
}
});
}
}
fireEvent(this, "value-changed", {
value,
});
}
private _negativeChanged(ev) {
ev.stopPropagation();
const negative = (ev.detail?.value || ev.target.value) === "-";
this._toggleNegative = negative;
const value = this.data;
if (value) {
FIELDS.forEach((t) => {
if (value[t]) {
value[t] = negative ? -Math.abs(value[t]) : Math.abs(value[t]);
}
});
fireEvent(this, "value-changed", {
value,
});
}
}
static styles = css`
.row {
display: flex;
align-items: center;
}
ha-button-toggle-group {
margin: var(--ha-space-2);
}
`;
}
declare global {

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