Compare commits

..
Author SHA1 Message Date
Bram Kragten 957bf875ae make it an option to track scrolling 2025-05-07 13:28:22 +02:00
Bram Kragten 3804a4c7cb Remove padding from scroll badges 2025-05-07 13:02:07 +02:00
b823a3b139 Fix badge overlay in scrolling mode (#25352)
Co-authored-by: Bram Kragten <[email protected]>
2025-05-07 10:39:25 +00:00
Bram KragtenandGitHub 47c9a407e6 Only enable drag scroll controller when needed (#25351) 2025-05-07 12:30:26 +02:00
Petar PetrovandGitHub c0ba48beb6 Set <=0 chart values to null so they render as gaps on a log graph (#25347) 2025-05-07 10:43:30 +02:00
Paulus SchoutsenandGitHub 075e1df204 Update network browser descriptions (#25345) 2025-05-07 10:40:38 +02:00
WendelinandGitHub 22c57853b4 Fix data-table search clear button alignment (#25348) 2025-05-07 11:30:36 +03:00
Bram KragtenandGitHub fe824062a5 fix height of stats graph card in section (#25344) 2025-05-07 08:11:34 +03:00
Paul BotteinandGitHub 92bf9b4979 Simplify entity combo-box code (#25338) 2025-05-06 20:44:12 +02:00
Paul BotteinandGitHub ac616a4d3d Use right theme variable for dashboard tab color in edit mode (#25339) 2025-05-06 17:05:44 +02:00
Paul BotteinandGitHub 1aa1bfda2c Use middle dot 00B7 as separator (#25336) 2025-05-06 15:56:20 +02:00
Paul BotteinandGitHub 38a5035d68 Fix outlined icon button style (#25340) 2025-05-06 15:30:04 +02:00
042cd0d3a3 Fix sidebar item text width (#25332)
* Fix sidebar item text width to utilize full available space

* Update src/components/ha-sidebar.ts

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

---------

Co-authored-by: Bram Kragten <[email protected]>
2025-05-06 11:52:28 +00:00
WendelinandGitHub 00d708fbd4 Fix flow form padding end (#25328)
* Fix flow form padding end

* Use more end padding if docs are present
2025-05-06 13:52:00 +02:00
WendelinandGitHub 852278e8aa Add custom retention info to backup locations (#25318)
* Add retention messages to backup locations

* Fix mobile

* Use join
2025-05-06 13:50:23 +02:00
Paul BotteinandGitHub 15dcdffe55 Add covers to overview view for area strategy (#25334) 2025-05-06 13:38:38 +02:00
WendelinandGitHub 0729aaacb8 Revert "Keyboard accessible panel sorting" (#25331) 2025-05-06 11:00:37 +02:00
Paul BotteinandGitHub 92b8cd8f45 Align side bar title with items (#25330) 2025-05-06 10:57:47 +02:00
Simon LamonandGitHub ad8d3dd598 Bump the sortable patch (#25262) 2025-05-06 09:52:04 +02:00
d618c25095 Show voice ID in TTS media browser (#25324)
* Show voice ID in TTS media browser

* Apply suggestions from code review

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

* Add copy button

* Now copy correct clipboard icon

* Improve styling

* GAP

---------

Co-authored-by: Paul Bottein <[email protected]>
2025-05-06 09:49:35 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
83289bdd41 Update dependency eslint to v9.26.0 (#25327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-06 09:43:21 +02:00
Paulus SchoutsenandGitHub c7882f3926 Populate integration domain My link (#25322)
* Populate integration domain My link

* break out of loop

* Actually just return from function

* Consolidate code
2025-05-05 17:48:29 +00:00
Paul BotteinandGitHub 7434b12d9f Use new entity naming in card entity picker (#25316) 2025-05-05 20:37:47 +03:00
Paulus SchoutsenandGitHub 9081441d95 Clean up network browser nav (#25321)
* Clean up network browser nav

* Add ha-md-list
2025-05-05 17:29:24 +00:00
WendelinandGitHub d63f610023 Fix options and repair flow success (#25312) 2025-05-05 19:20:22 +02:00
karwostsandGitHub e069875432 Add energy hourly calculations to CSV report (#25315) 2025-05-05 13:09:16 -04:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
91026b0986 Update dependency @codemirror/view to v6.36.7 (#25320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 17:06:05 +00:00
Paul BotteinandGitHub 4ec5fbc9a4 Do not display no areas in entity pickers (#25317) 2025-05-05 18:56:16 +02:00
Paulus SchoutsenandGitHub fb3a59272d Reorder my links (#25319) 2025-05-05 18:54:33 +02:00
WendelinandGitHub 9155c85509 Fix zwave add device LR/mesh icons (#25313)
Fix zwave LR/mesh icons
2025-05-05 09:05:35 -04:00
WendelinandGitHub 9d74cd7561 Fix selected entity overflow (#25311) 2025-05-05 13:11:42 +02:00
Paul BotteinandGitHub 22ddcca954 Fix pasting yaml in automation code editor (#25309)
Fix pasting yaml in code editor
2025-05-05 11:42:21 +02:00
WendelinandGitHub 8f422357f1 Fix select entity change (#25310) 2025-05-05 09:13:50 +00:00
Jan-Philipp BeneckeandGitHub 44f5f7bdb5 Use new entity picker style in quick bar (#25265)
* Use new entity picker style in quick bar

* Cleanup

* Add missing no area

* Process code review
2025-05-05 11:12:43 +02:00
WendelinandGitHub 83819a9be0 Revert "Use md-select for entity-row and state-card" (#25308)
Revert "Use md-select for entity-row and state-card (#25307)"

This reverts commit 3c9dce20e2.
2025-05-05 08:57:50 +00:00
WendelinandGitHub 3c9dce20e2 Use md-select for entity-row and state-card (#25307) 2025-05-05 11:01:35 +03:00
WendelinandGitHub a19e7002ea Fix flow-form header (#25305) 2025-05-05 10:40:54 +03:00
Paulus SchoutsenandGitHub 75608db9b8 Add profile security link to My Home Assistant (#25303) 2025-05-05 08:33:52 +03:00
karwostsandGitHub 08f30b714b Keyboard accessible panel sorting (#25288) 2025-05-04 14:58:41 +03:00
Volodymyr HoncharandGitHub 9983129e26 Add a feature to display any attribute of an entity in the Gauge Card (#25258)
* Add a feature to display any attribute of an entity in the Gauge Card

* Extract list of non numeric attributes into a constant and reuse it in number card as well as in numeric condition
2025-05-04 11:51:01 +00:00
Bram KragtenandGitHub 40fe62c2ec Fix select entity row opening more info on select (#25292) 2025-05-04 14:41:01 +03:00
Bram KragtenandGitHub d7660370ab Fix display of disabled items in traces (#25293) 2025-05-04 09:38:01 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
bdad76937e Update babel monorepo to v7.27.1 (#25289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-04 09:25:12 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
d2822308ec Update dependency @lokalise/node-api to v14.5.0 (#25290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-04 09:24:51 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
99b94e799d Update dependency @bundle-stats/plugin-webpack-filter to v4.20.0 (#25285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-03 08:53:39 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
1fb28df1a6 Update dependency core-js to v3.42.0 (#25283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-02 21:28:27 +02:00
b4e8c56f58 Fix decorators with properties (#25282)
* Fix decorators with properties

* Green build

---------

Co-authored-by: Simon Lamon <[email protected]>
2025-05-02 18:21:38 +02:00
Bram KragtenandGitHub 17cc63deba Fix alignment of ha-labeled-slider (#25279) 2025-05-02 10:47:20 -04:00
karwostsandGitHub b4f1c8755d Fix disabled language picker (#25278) 2025-05-02 16:19:12 +03:00
J. Nick KostonandGitHub b0d4c699db Add my links for the Bluetooth monitors (#25270)
The plan is to link these in the Bluetooth docs for help debugging
2025-05-02 12:46:36 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
c07bf68161 Update dependency typescript-eslint to v8.31.1 (#25272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-02 12:16:28 +02:00
Jan-Philipp BeneckeandGitHub d1a0eaece5 Add save shortcut to shortcuts dialog (#25271) 2025-05-02 12:16:04 +02:00
Bram KragtenandGitHub f608783551 Improve error handling in automation i18n (#25266) 2025-05-01 19:06:42 +02:00
Jan-Philipp BeneckeandGitHub dddba58d38 Always show backup location retention settings (#25261)
Always show backup location retention settings
2025-05-01 19:02:55 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
ebc16d6520 Update rspack monorepo to v1.3.8 (#25267)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-01 19:01:39 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4ed8ecad01 Update vaadinWebComponents monorepo to v24.7.5 (#25268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-01 19:01:35 +02:00
Paulus SchoutsenandGitHub c26fb1713d Import missing components on init page (#25269) 2025-05-01 15:57:56 +03:00
Jan-Philipp BeneckeandGitHub 2b7b17625e Hide the tab when view is a subview (#25256) 2025-05-01 09:25:18 +02:00
karwostsandGitHub cd3e4f55e2 Fix typo in energy calculation (#25259)
* New energy calculation

* more tests and stricter tests. change priority order

* more test and fix error
2025-05-01 07:27:11 +03:00
Jan-Philipp BeneckeandGitHub 1c12aea8f6 Add ? as shortcut for shortcuts dialog (#25253)
Bind shortcuts dialog to `?` key
2025-04-30 22:23:45 +02:00
J. Nick KostonandGitHub 3722f971ca Improve message when no discovery data is found (#25252)
* Improve message when no discovery data is found

It was pointed out its a bit confusing when a device has not been
discovered yet for the discovery/network browser panels as we
only said there was no data. Give the user a better hint
as to why there is no data.

* Improve message when no discovery data is found

It was pointed out its a bit confusing when a device has not been
discovered yet for the discovery/network browser panels as we
only said there was no data. Give the user a better hint
as to why there is no data.

* Improve message when no discovery data is found

It was pointed out its a bit confusing when a device has not been
discovered yet for the discovery/network browser panels as we
only said there was no data. Give the user a better hint
as to why there is no data.

* Improve message when no discovery data is found

It was pointed out its a bit confusing when a device has not been
discovered yet for the discovery/network browser panels as we
only said there was no data. Give the user a better hint
as to why there is no data.
2025-04-30 21:53:07 +02:00
J. Nick KostonandGitHub 409f665641 Fix formatting of mac address fields in device info (#25251)
Previous change lost the `:` seperator, and unexpectedly MAC became
title case instead of MAC when dhcp is loaded.
2025-04-30 19:16:25 +00:00
J. Nick KostonandGitHub 5b3b17ef6d Better explain when DHCP discovery data will be available (#25250)
It was pointed out that users likely may not know what DHCP is
and wonder why the data is not available yet.
2025-04-30 19:10:32 +00:00
Yosi LevyandGitHub 05b49e8c80 Various RTL fixes (#25231) 2025-04-30 21:07:55 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2dbdbb4b64 Update dependency eslint-plugin-wc to v3.0.1 (#25249)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-30 20:50:34 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a825b632bf Bump vite from 6.3.2 to 6.3.4 (#25248)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.2 to 6.3.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.4/packages/vite)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-30 20:34:02 +02:00
karwostsandGitHub f8d706277d New energy calculation formula (#25242)
* New energy calculation

* more tests and stricter tests. change priority order
2025-04-30 19:47:51 +03:00
Bram KragtenandGitHub 221bc732fb Handle errrors/wrong values on paste (#25245) 2025-04-30 17:45:35 +02:00
Paul BotteinandGitHub 055c18463c Use code font family variable in combo-box (#25243) 2025-04-30 17:17:01 +03:00
Bram KragtenandGitHub ddd51ff097 Improve trigger condition check on paste (#25241) 2025-04-30 16:02:53 +02:00
Bram KragtenandGitHub 55c75096d0 Allow pasting more script config (#25240)
* Allow pasting more script config

* Update manual-script-editor.ts
2025-04-30 13:15:10 +00:00
Bram KragtenandGitHub fd3502f3bc Allow pasting more automation config formats (#25239) 2025-04-30 14:57:16 +02:00
J. Nick KostonandGitHub 7d6bec01ae Add DHCP Browser entry point to network (#25235)
* Add DHCP Browser entry point to network

* lint
2025-04-30 08:51:22 -04:00
Paul Bottein 0aeb8fa75c Merge branch 'rc' into dev 2025-04-30 12:53:10 +02:00
Paul Bottein 456a44fdfd Bumped version to 20250430.0 2025-04-30 12:35:26 +02:00
Paul BotteinandGitHub 53a0b311de Use list item for statistic picker (#25228)
* Use list item for statistic picker

* Use item interface

* Fix icon
2025-04-30 11:40:56 +03:00
+1
Petar PetrovGitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Erik MontnemeryWendelinWendelinrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Yosi LevyBram Kragtenemufan
ded5ade0f2 Grouping options for hui-energy-sankey-card (#25207)
* Grouping options for hui-energy-sankey-card

* use getEntityContext for area/floor

* Update rspack monorepo to v1.3.7 (#25206)

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

* Update vaadinWebComponents monorepo to v24.7.4 (#25204)

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

* Add typography styles (#25171)

* Add typoghrapy styles

* Split styles

* Fix duplicated html

* remove unused paper vars

* Fix vars

* Add vars autocompletion extension

* Fix css syntax highlighting

* History tooltip RTL fix (#24917)

* History tooltip RTL fix

* Fix background color

* Render todo items with no state, change look of read only items (#24529)

* Render todo items with no state, change look of read only items

Co-authored-by: Erik Montnemery <[email protected]>
Co-authored-by: Wendelin <[email protected]>
Co-authored-by: Wendelin <[email protected]>

* Themeable badge icon size and badge font size (#25185)

Co-authored-by: Wendelin <[email protected]>

* Add context to config update entries (#25208)

* Add context to config update entries

* Add no area

* import fix

* import fix

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Wendelin <[email protected]>
Co-authored-by: Yosi Levy <[email protected]>
Co-authored-by: Bram Kragten <[email protected]>
Co-authored-by: Erik Montnemery <[email protected]>
Co-authored-by: Wendelin <[email protected]>
Co-authored-by: emufan <[email protected]>
2025-04-30 11:00:51 +03:00
J. Nick KostonandGitHub 5d2d6dcd6c Make name a top level field for the SSDP panel (#25232)
* Make name a top level field for the SSDP panel

* Make name a top level field for the SSDP panel
2025-04-30 09:39:01 +02:00
92353ebed5 Centralize energy usage calculations (#25197)
* Centralize energy usage calculations

* addl tests

* test organization

* Update src/data/energy.ts

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

* Centralize more equations

---------

Co-authored-by: Petar Petrov <[email protected]>
2025-04-30 06:57:26 +03:00
J. Nick KostonandGitHub bc582db7fc Add initial SSDP discovery panel (#25217)
* Add initial SSDP discovery panel
2025-04-29 21:46:56 +02:00
Jan-Philipp BeneckeandGitHub ab415188ba Allow pasting YAML in automation/script editor directly (#24838)
* Allow pasting YAML in automation/script editor directly

* Highlight pasted items

* Change highlighting

* Also reset in script editor

* Show dialog when pasting to changed automation/script

* Add shortcuts to shortcuts dialog

* Use translated shortcut

* Change timeout and clear on save

* Fix imports

* Process code review

* Move paste logic to manual-*-editor
2025-04-29 18:28:07 +02:00
WendelinandGitHub 29c11978b3 Rename font-weight-semibold to medium (#25224) 2025-04-29 16:50:30 +02:00
Paul BotteinandGitHub 574f9e8936 Add message when no entities found in the entity picker (#25222) 2025-04-29 12:34:09 +00:00
WendelinandGitHub fddc00bfab Change --ha-font-weight-bold (#25221)
* Use font-weight vars in roboto

* Revert roboto usage of vars
2025-04-29 14:24:06 +02:00
Paul Bottein ff5cbb0613 Revert "Add message in picker when no entities found"
This reverts commit 498d933c06.
2025-04-29 14:22:26 +02:00
Paul Bottein 498d933c06 Add message in picker when no entities found 2025-04-29 14:14:38 +02:00
f9fbb254bf Use list item for entity picker (#25176)
Co-authored-by: Wendelin <[email protected]>
Co-authored-by: Wendelin <[email protected]>
2025-04-29 13:30:32 +02:00
Paul BotteinandGitHub 536602580d Remove duplicated context files (#25212) 2025-04-29 11:51:05 +02:00
WendelinandGitHub c111bf1062 Add ha-badge docs to gallery (#25218) 2025-04-29 11:37:56 +02:00
Simon LamonandGitHub 0242fbc6f8 Guard against unknown entities in automation compare functions (#25213)
Guard against unknown entities
2025-04-29 08:24:49 +03:00
WendelinandGitHub f65a0ef4f7 Add context to config update entries (#25208)
* Add context to config update entries

* Add no area
2025-04-28 18:43:10 +02:00
Bram Kragten 6a333a4774 Bumped version to 20250411.0 2025-04-11 14:45:42 +02:00
Paul BotteinandBram Kragten 7742ccf631 Only ask to refresh dashboard if necessary (#24993) 2025-04-11 14:45:07 +02:00
Paul BotteinandBram Kragten 20f2a8d53e Fix refresh strategy config on HA start-up (#24984) 2025-04-11 14:45:07 +02:00
Bram Kragten ec9fbe7d77 fix dropdown behind datatable (#24981) 2025-04-11 14:45:06 +02:00
WendelinandBram Kragten 6fa226d30a Fix submit spinner in config-flow-form (#24969) 2025-04-11 14:45:04 +02:00
Bram Kragten b76a723fd9 fix voice wizard bugs (#24950) 2025-04-11 14:44:09 +02:00
Paul BotteinandBram Kragten 5237cc72b7 Restore default hold action for some cards (#24947) 2025-04-11 14:42:20 +02:00
Jan-Philipp BeneckeandBram Kragten 63d2718f67 Import missing ha-tip in quick bar dialog (#24929) 2025-04-11 14:42:19 +02:00
Bram Kragten 1e26f155a7 Bumped version to 20250404.0 2025-04-04 15:10:56 +02:00
Simon LamonandBram Kragten 27e13017c3 Show the correct area icon in entity breadcrumb (#24913) 2025-04-04 15:10:37 +02:00
Paul BotteinandBram Kragten 88f1dc9c16 Add missing translations for areas strategy (#24905) 2025-04-04 15:10:36 +02:00
Paul BotteinandBram Kragten 825e707a80 Refresh dashboard strategy when registries changed (#24902)
* Refresh dashboard strategy when registries changed

* Display toast before refreshing dashboard

* Apply suggestions
2025-04-04 15:10:35 +02:00
Bram Kragten 0bcaa104e7 Bumped version to 20250401.0 2025-04-01 17:30:31 +02:00
Bram Kragten 6b3f807129 Developer tools action fixes (#24876) 2025-04-01 17:30:16 +02:00
Paul BotteinandBram Kragten c464d344db Add ellipsis for more info breadcrumb (#24882) 2025-04-01 17:29:14 +02:00
karwostsandBram Kragten 69f0a4a728 Fix condition rendering in trace choose node (#24878) 2025-04-01 17:29:13 +02:00
Bram Kragten 2ba8f9f99d Bumped version to 20250331.0 2025-03-31 20:43:31 +02:00
Bram Kragten 7e06bbc467 Fix add zwave device my link (#24871) 2025-03-31 20:42:38 +02:00
Paul BotteinandBram Kragten 6017d82c21 Handle date range shift during daylight saving time days (#24868) 2025-03-31 20:42:37 +02:00
Bram Kragten 40c200a172 fix spinner in tts try dialog (#24867) 2025-03-31 20:42:36 +02:00
Bram Kragten a2f70f682f Take lang into account when search existing pipeline (#24866)
* Take lang into account when search existing pipeline

* Simplify logic
2025-03-31 20:42:36 +02:00
Paul BotteinandBram Kragten c42a899b52 Force clock card to display time LTR (#24865) 2025-03-31 20:42:35 +02:00
Paul BotteinandBram Kragten 706f43e99e Add interactions for weather card editor (#24864) 2025-03-31 20:42:34 +02:00
karwostsandBram Kragten f5496c21e8 Bar charts start from 0 (#24854) 2025-03-31 20:42:33 +02:00
Paul BotteinandBram Kragten 34dce5b279 Only use button for breadcrumb for admin users (#24836) 2025-03-31 20:42:32 +02:00
Bram Kragten a4f07423ec Name local pipeline based on local or full choice (#24835) 2025-03-31 20:42:31 +02:00
Bram Kragten 9e32c24f3c Update lang support text in voice wizard (#24834) 2025-03-31 20:42:30 +02:00
Paul BotteinandBram Kragten b281d095cd Remove add-on word in satellite wizard translations for state (#24832) 2025-03-31 20:42:29 +02:00
Paul BotteinandBram Kragten fe7e8e17ae More info breadcrumb clickable (#24830)
* Make more info breadcrum clickable

* css adjustements
2025-03-28 15:37:27 +01:00
Eloy RodriguezandBram Kragten 2161357226 Add title and time zone to clock card (#24818)
* Add title and time zone to clock card

* Small changes to the spacing and text sizing of the clock card

* Update translations to use dropdown labels from profile configuration

* Use similar approach as #24819 for setting automatic time zone

* Update hui-clock-card.ts

---------

Co-authored-by: Bram Kragten <[email protected]>
2025-03-28 15:37:02 +01:00
Darren GriffinandBram Kragten e8e65a4293 Fix default time_format option. Fixes #24798 (#24819)
* Fix default time_format option. Fixes #24798

* Update en.json

* Update src/translations/en.json

---------

Co-authored-by: Bram Kragten <[email protected]>
2025-03-28 15:36:29 +01:00
Bram Kragten 724adab2d6 Bumped version to 20250328.0 2025-03-28 15:02:51 +01:00
Bram Kragten 345ad6c9c5 Update voice-assistant-setup-step-local.ts 2025-03-28 15:02:37 +01:00
Bram Kragten a88d066d7e Update text voice wizard install addons step (#24829) 2025-03-28 15:02:15 +01:00
Paulus SchoutsenandBram Kragten a8e5c8482b Hide backup from default dashboard (#24828) 2025-03-28 15:02:14 +01:00
Paulus SchoutsenandBram Kragten d5ff8ab1e1 Do not play pre-announce sound when testing voice on satellite (#24827) 2025-03-28 15:02:13 +01:00
Bram KragtenandPaulus Schoutsen e765cc10fb Fix voice flow (#24825)
* Fix voice flow

* Apply suggestions from code review

---------

Co-authored-by: Paulus Schoutsen <[email protected]>
2025-03-28 15:02:12 +01:00
Paul BotteinandBram Kragten 916dec101f Add hold and double tap action in the UI for every card that supports it. (#24824)
* Add double tap action to button card UI editor

* Add double tap action to light card UI editor

* Add hold action and double tap action to gauge card UI editor

* Add hold action and double tap action to picture glance card UI editor

* Add hold action and double tap action to picture card UI editor

* Add hold action and double tap action to entity card UI editor

* Add hold action and double tap action to elements
2025-03-28 15:02:11 +01:00
Paul BotteinandBram Kragten 909fc119b7 Add scroll restoration when using back navigation in dashboard (#24822)
Add scroll restoration when using back navigation with subviews
2025-03-28 15:02:10 +01:00
puddlyandBram Kragten 8751dc46f4 Show hardware integrations in the integration list (#24820)
Show hardware integrations in the frontend
2025-03-28 15:02:09 +01:00
Paul Bottein 118c25d25f Bumped version to 20250327.1 2025-03-27 19:12:22 +01:00
Paul Bottein ae5427a75e Fix dashboard strategy (#24808) 2025-03-27 19:12:03 +01:00
Paul Bottein 3b6e267fb5 Fallback to state name when the entry doesn't have name (#24805) 2025-03-27 19:12:02 +01:00
Bram Kragten 1770a51303 Bumped version to 20250327.0 2025-03-27 16:46:17 +01:00
Paul BotteinandBram Kragten 534df3d378 Add loading state to area strategy (#24803) 2025-03-27 16:44:15 +01:00
Paul BotteinandBram Kragten 23229b3e3b Set the max number of columns to 3 for area dashboard (#24802)
* Set the max number of columns to 4 for area dashboard

* Set it to 3
2025-03-27 16:44:14 +01:00
karwostsandBram Kragten 94ee99160b Energy device settings fixes (#24801) 2025-03-27 16:44:13 +01:00
Paul BotteinandBram Kragten b009d71e8f Fix take control of the dashboard (#24800) 2025-03-27 16:44:12 +01:00
Bram Kragten 2ab8209622 Align behavior of template selector with text selector (#24796) 2025-03-27 16:44:11 +01:00
Paul BotteinandBram Kragten ed2940edc3 Revert "Restore scroll position when using back navigation in dashboard" (#24795)
Revert "Restore scroll position when using back navigation in dashboard (#24777)"

This reverts commit 9cfcd21a93.
2025-03-27 16:44:11 +01:00
Paul BotteinandBram Kragten e2b9a06242 Fix more info for disabled entities (#24789) 2025-03-27 16:44:10 +01:00
Paul BotteinandBram Kragten a7acee0438 Remove fixed height in ha tile info (#24787)
Remove unused height in ha tile info
2025-03-27 16:44:09 +01:00
Bram Kragten 1208af510c Fix typo in Arithmetic (#24786)
Fix type in Arithmetic
2025-03-27 16:44:08 +01:00
398 changed files with 7399 additions and 3739 deletions
+1 -1
View File
@@ -302,7 +302,7 @@ export class HcConnect extends LitElement {
}
.error {
color: red;
font-weight: var(--ha-font-weight-bold);
font-weight: bold;
}
.error a {
+5 -5
View File
@@ -86,9 +86,9 @@ class HcLayout extends LitElement {
.card-header {
color: var(--ha-card-header-color, var(--primary-text-color));
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;
line-height: var(--ha-line-height-normal);
line-height: 32px;
padding: 24px 16px 16px;
display: block;
margin: 0;
@@ -98,7 +98,7 @@ class HcLayout extends LitElement {
border-radius: 4px 4px 0 0;
}
.subtitle {
font-size: var(--ha-font-size-m);
font-size: 14px;
color: var(--secondary-text-color);
line-height: initial;
}
@@ -113,7 +113,7 @@ class HcLayout extends LitElement {
}
:host ::slotted(.section-header) {
font-weight: var(--ha-font-weight-semibold);
font-weight: 500;
padding: 4px 16px;
text-transform: uppercase;
}
@@ -135,7 +135,7 @@ class HcLayout extends LitElement {
.footer {
text-align: center;
font-size: var(--ha-font-size-s);
font-size: 12px;
padding: 8px 0 24px;
color: var(--secondary-text-color);
}
+1 -1
View File
@@ -29,7 +29,7 @@ class HcLaunchScreen extends LitElement {
display: block;
height: 100vh;
background-color: #f2f4f9;
font-size: var(--ha-font-size-2xl);
font-size: 24px;
}
.container {
display: flex;
+1 -1
View File
@@ -50,7 +50,7 @@
font-family: Roboto, Noto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-weight: var(--ha-font-weight-normal);
font-weight: 400;
height: 100vh;
margin: 0;
padding: 0;
+4 -4
View File
@@ -37,13 +37,13 @@ class PageDescription extends HaMarkdown {
border-bottom: 1px solid var(--secondary-background-color);
}
.title {
font-size: var(--ha-font-size-5xl);
line-height: var(--ha-line-height-normal);
font-size: 42px;
line-height: 56px;
padding-bottom: 8px;
}
.subtitle {
font-size: var(--ha-font-size-l);
line-height: var(--ha-line-height-normal);
font-size: 18px;
line-height: 24px;
}
.root {
max-width: 800px;
+1 -1
View File
@@ -1,5 +1,5 @@
import "./ha-gallery";
import("../../src/resources/ha-style");
import("../../src/resources/append-ha-style");
document.body.appendChild(document.createElement("ha-gallery"));
+1 -1
View File
@@ -34,7 +34,7 @@ class HaDemoOptions extends LitElement {
height: 64px;
padding: 0 16px;
pointer-events: none;
font-size: var(--ha-font-size-xl);
font-size: 20px;
}
`,
];
+4 -4
View File
@@ -250,14 +250,14 @@ class HaGallery extends LitElement {
}
.page-footer .header {
font-size: var(--ha-font-size-l);
font-weight: var(--ha-font-weight-semibold);
line-height: var(--ha-line-height-normal);
font-size: 16px;
font-weight: 500;
line-height: 28px;
text-align: center;
}
.page-footer .secondary {
line-height: var(--ha-line-height-normal);
line-height: 23px;
text-align: center;
}
@@ -0,0 +1,65 @@
---
title: Badge
subtitle: Lovelace dashboard badge
---
<style>
.wrapper {
display: flex;
gap: 24px;
}
</style>
# Badge `<ha-badge>`
The badge component is a small component that displays a number or status information. It is used in the lovelace dashboard on the top.
## Implementation
### Example Usage
<div class="wrapper">
<ha-badge>
simple badge
</ha-badge>
<ha-badge label="Info">
With a label
</ha-badge>
<ha-badge type="button">
Type button
</ha-badge>
</div>
```html
<ha-badge> simple badge </ha-badge>
<ha-badge label="Info"> With a label </ha-badge>
<ha-badge type="button"> Type button </ha-badge>
```
### API
**Slots**
- default slot is the content of the badge
- no default
- `icon` set the icon of the badge
- no default
**Properties/Attributes**
| Name | Type | Default | Description |
| -------- | ----------------------- | ----------- | ------------------------------------------------------------ |
| type | `"badge"` or `"button"` | `"badge"` | If it's button it shows a ripple effect |
| label | string | `undefined` | Text label for the badge, only visible if `iconOnly = false` |
| iconOnly | boolean | `false` | Only show label |
**CSS Custom Properties**
- `--ha-badge-size` (default `36px`)
- `--ha-badge-border-radius` (default `calc(var(--ha-badge-size, 36px) / 2)`)
- `--ha-badge-font-size` (default `var(--ha-font-size-s)`)
- `--ha-badge-icon-size` (default `18px`)
+129
View File
@@ -0,0 +1,129 @@
import { mdiButtonCursor, mdiHome } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/ha-badge";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-svg-icon";
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
const badges: {
type?: "badge" | "button";
label?: string;
iconOnly?: boolean;
slot?: TemplateResult;
iconSlot?: TemplateResult;
}[] = [
{
slot: html`<span>Badge</span>`,
},
{
type: "badge",
label: "Badge",
iconSlot: html`<ha-svg-icon slot="icon" .path=${mdiHome}></ha-svg-icon>`,
slot: html`<span>Badge</span>`,
},
{
type: "button",
label: "Button",
iconSlot: html`<ha-svg-icon
slot="icon"
.path=${mdiButtonCursor}
></ha-svg-icon>`,
slot: html`<span>Button</span>`,
},
{
type: "button",
label: "Label only",
iconSlot: html`<ha-svg-icon
slot="icon"
.path=${mdiButtonCursor}
></ha-svg-icon>`,
},
{
type: "button",
label: "Label",
slot: html`<span>Button no label</span>`,
},
{
label: "Icon only",
iconOnly: true,
iconSlot: html`<ha-svg-icon
slot="icon"
.path=${mdiHomeAssistant}
></ha-svg-icon>`,
},
];
@customElement("demo-components-ha-badge")
export class DemoHaBadge extends LitElement {
protected render(): TemplateResult {
return html`
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-badge ${mode} demo">
<div class="card-content">
${badges.map(
(badge) => html`
<ha-badge
.type=${badge.type || undefined}
.label=${badge.label}
.iconOnly=${badge.iconOnly || false}
>
${badge.iconSlot} ${badge.slot}
</ha-badge>
`
)}
</div>
</ha-card>
</div>
`
)}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
static styles = css`
:host {
display: flex;
justify-content: center;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
}
ha-card {
margin: 24px auto;
}
.card-content {
display: flex;
gap: 24px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-badge": DemoHaBadge;
}
}
@@ -150,7 +150,7 @@ export class DemoHaBarButton extends LitElement {
margin: 0;
}
label {
font-weight: var(--ha-font-weight-bold);
font-weight: 600;
}
.custom {
--control-button-icon-color: var(--primary-color);
@@ -86,7 +86,7 @@ export class DemoHarControlNumberButtons extends LitElement {
margin: 0;
}
label {
font-weight: var(--ha-font-weight-bold);
font-weight: 600;
}
.custom {
color: #2196f3;
@@ -125,7 +125,7 @@ export class DemoHaControlSelectMenu extends LitElement {
margin: 0;
}
label {
font-weight: var(--ha-font-weight-bold);
font-weight: 600;
}
.custom {
--control-button-icon-color: var(--primary-color);
@@ -181,7 +181,7 @@ export class DemoHaControlSelect extends LitElement {
margin: 0;
}
label {
font-weight: var(--ha-font-weight-bold);
font-weight: 600;
}
.custom {
--mdc-icon-size: 24px;
@@ -144,7 +144,7 @@ export class DemoHaBarSlider extends LitElement {
margin: 0;
}
label {
font-weight: var(--ha-font-weight-bold);
font-weight: 600;
}
.custom {
--control-slider-color: #ffcf4c;
@@ -112,7 +112,7 @@ export class DemoHaControlSwitch extends LitElement {
margin: 0;
}
label {
font-weight: var(--ha-font-weight-bold);
font-weight: 600;
}
.custom {
--control-switch-on-color: var(--green-color);
@@ -105,8 +105,8 @@ export class DemoHaHsColorPicker extends LitElement {
width: 400px;
}
.value {
font-size: var(--ha-font-size-2xl);
font-weight: var(--ha-font-weight-bold);
font-size: 22px;
font-weight: bold;
margin: 0 0 12px 0;
}
`;
@@ -123,7 +123,7 @@ export class DemoHaSelectBox extends LitElement {
margin: 0;
}
label {
font-weight: var(--ha-font-weight-bold);
font-weight: 600;
margin-bottom: 8px;
display: block;
}
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeNumeric extends LitElement {
margin: 12px auto;
}
.header {
font-weight: var(--ha-font-weight-bold);
font-weight: bold;
}
.center {
text-align: center;
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeSeconds extends LitElement {
margin: 12px auto;
}
.header {
font-weight: var(--ha-font-weight-bold);
font-weight: bold;
}
.center {
text-align: center;
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeShortYear extends LitElement {
margin: 12px auto;
}
.header {
font-weight: var(--ha-font-weight-bold);
font-weight: bold;
}
.center {
text-align: center;
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeShort extends LitElement {
margin: 12px auto;
}
.header {
font-weight: var(--ha-font-weight-bold);
font-weight: bold;
}
.center {
text-align: center;
+1 -1
View File
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTime extends LitElement {
margin: 12px auto;
}
.header {
font-weight: var(--ha-font-weight-bold);
font-weight: bold;
}
.center {
text-align: center;
+1 -1
View File
@@ -92,7 +92,7 @@ export class DemoDateTimeDate extends LitElement {
static styles = css`
.header {
font-weight: var(--ha-font-weight-bold);
font-weight: bold;
}
.center {
text-align: center;
+1 -1
View File
@@ -106,7 +106,7 @@ export class DemoDateTimeTimeSeconds extends LitElement {
margin: 12px auto;
}
.header {
font-weight: var(--ha-font-weight-bold);
font-weight: bold;
}
.center {
text-align: center;
+1 -1
View File
@@ -106,7 +106,7 @@ export class DemoDateTimeTimeWeekday extends LitElement {
margin: 12px auto;
}
.header {
font-weight: var(--ha-font-weight-bold);
font-weight: bold;
}
.center {
text-align: center;
+1 -1
View File
@@ -106,7 +106,7 @@ export class DemoDateTimeTime extends LitElement {
margin: 12px auto;
}
.header {
font-weight: var(--ha-font-weight-bold);
font-weight: bold;
}
.center {
text-align: center;
@@ -428,13 +428,13 @@ class HassioAddonConfig extends LitElement {
.header h2 {
color: var(--ha-card-header-color, var(--primary-text-color));
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;
line-height: var(--ha-line-height-expanded);
line-height: 48px;
padding: 12px 16px 16px;
display: block;
margin-block: 0px;
font-weight: var(--ha-font-weight-normal);
font-weight: normal;
}
.card-actions.right {
justify-content: flex-end;
@@ -1280,12 +1280,12 @@ class HassioAddonInfo extends LitElement {
padding-left: 8px;
padding-inline-start: 8px;
padding-inline-end: initial;
font-size: var(--ha-font-size-2xl);
font-size: 24px;
color: var(--ha-card-header-color, var(--primary-text-color));
}
.addon-version {
float: var(--float-end);
font-size: var(--ha-font-size-m);
font-size: 15px;
vertical-align: middle;
}
.errors {
+2 -2
View File
@@ -391,7 +391,7 @@ export class HassioBackups extends LitElement {
top: -4px;
}
.selected-txt {
font-weight: var(--ha-font-weight-bold);
font-weight: bold;
padding-left: 16px;
padding-inline-start: 16px;
padding-inline-end: initial;
@@ -401,7 +401,7 @@ export class HassioBackups extends LitElement {
margin-top: 20px;
}
.header-toolbar .selected-txt {
font-size: var(--ha-font-size-l);
font-size: 16px;
}
.header-toolbar .header-btns {
margin-right: -12px;
+1 -1
View File
@@ -101,7 +101,7 @@ class HassioCardContent extends LitElement {
overflow: hidden;
position: relative;
height: 2.4em;
line-height: var(--ha-line-height-condensed);
line-height: 1.2em;
}
.icon_image img {
max-height: 40px;
+1 -1
View File
@@ -131,7 +131,7 @@ export class HassioUpdate extends LitElement {
}
.update-heading {
font-size: var(--ha-font-size-l);
font-weight: var(--ha-font-weight-semibold);
font-weight: 500;
margin-bottom: 0.5em;
color: var(--primary-text-color);
}
@@ -169,7 +169,7 @@ class HassioHardwareDialog extends LitElement {
pre {
padding: 16px;
overflow: auto;
line-height: var(--ha-line-height-normal);
line-height: 1.45;
font-family: var(--ha-font-family-code);
}
code {
+1 -1
View File
@@ -8,7 +8,7 @@ body {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-weight: var(--ha-font-weight-normal);
font-weight: 400;
margin: 0;
padding: 0;
height: 100vh;
@@ -340,12 +340,12 @@ class HassioIngressView extends LitElement {
.header {
display: flex;
align-items: center;
font-size: var(--ha-font-size-l);
font-size: 16px;
height: 40px;
padding: 0 16px;
pointer-events: none;
background-color: var(--app-header-background-color);
font-weight: var(--ha-font-weight-normal);
font-weight: 400;
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
box-sizing: border-box;
@@ -354,7 +354,7 @@ class HassioIngressView extends LitElement {
.main-title {
margin: var(--margin-title);
line-height: var(--ha-line-height-normal);
line-height: 20px;
flex-grow: 1;
}
+1 -1
View File
@@ -1,3 +1,3 @@
import "./ha-landing-page";
import("../../src/resources/ha-style");
import("../../src/resources/append-ha-style");
+16 -16
View File
@@ -26,7 +26,7 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.27.0",
"@babel/runtime": "7.27.1",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/commands": "6.8.1",
@@ -34,7 +34,7 @@
"@codemirror/legacy-modes": "6.5.1",
"@codemirror/search": "6.5.10",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.36.6",
"@codemirror/view": "6.36.7",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.0",
"@formatjs/intl-displaynames": "6.8.11",
@@ -89,8 +89,8 @@
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.8.1",
"@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.7.4",
"@vaadin/vaadin-themable-mixin": "24.7.4",
"@vaadin/combo-box": "24.7.5",
"@vaadin/vaadin-themable-mixin": "24.7.5",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
@@ -99,7 +99,7 @@
"barcode-detector": "3.0.1",
"color-name": "2.0.0",
"comlink": "4.4.2",
"core-js": "3.41.0",
"core-js": "3.42.0",
"cropperjs": "1.6.2",
"date-fns": "4.1.0",
"date-fns-tz": "3.2.0",
@@ -131,7 +131,7 @@
"qrcode": "1.5.4",
"roboto-fontface": "0.10.0",
"rrule": "2.8.1",
"sortablejs": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch",
"sortablejs": "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch",
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
@@ -150,18 +150,18 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.26.10",
"@babel/core": "7.27.1",
"@babel/helper-define-polyfill-provider": "0.6.4",
"@babel/plugin-transform-runtime": "7.26.10",
"@babel/preset-env": "7.26.9",
"@bundle-stats/plugin-webpack-filter": "4.19.1",
"@lokalise/node-api": "14.4.0",
"@babel/plugin-transform-runtime": "7.27.1",
"@babel/preset-env": "7.27.1",
"@bundle-stats/plugin-webpack-filter": "4.20.0",
"@lokalise/node-api": "14.5.0",
"@octokit/auth-oauth-device": "7.1.5",
"@octokit/plugin-retry": "7.2.1",
"@octokit/rest": "21.1.1",
"@rsdoctor/rspack-plugin": "1.0.2",
"@rspack/cli": "1.3.7",
"@rspack/core": "1.3.7",
"@rspack/cli": "1.3.8",
"@rspack/core": "1.3.8",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.21",
"@types/chromecast-caf-sender": "1.0.11",
@@ -185,7 +185,7 @@
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.0",
"eslint": "9.25.1",
"eslint": "9.26.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.2",
"eslint-import-resolver-webpack": "0.13.10",
@@ -193,7 +193,7 @@
"eslint-plugin-lit": "2.1.1",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "3.0.0",
"eslint-plugin-wc": "3.0.1",
"fancy-log": "2.0.0",
"fs-extra": "11.3.0",
"glob": "11.0.2",
@@ -219,7 +219,7 @@
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.8.3",
"typescript-eslint": "8.31.0",
"typescript-eslint": "8.31.1",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.1.2",
"webpack-stats-plugin": "1.1.3",
@@ -1,13 +1,13 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8283 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 39.4999L76.9105 39.4999V36.4999L37.5 36.4999L37.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8284 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 37.9999L37.5 39.4999L76.9105 39.4999V37.9999V36.4999L37.5 36.4999L37.5 37.9999Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M30.8239 22.3365L38.8239 38.8365L30.3239 50.3365" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
<mask id="mask0_1110_23734" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<mask id="mask0_2_779" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1110_23734)">
<rect x="30" y="27" width="18" height="18" fill="#212121"/>
<g mask="url(#mask0_2_779)">
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
</g>
<path d="M82 37.9999C82 36.343 83.3431 34.9999 85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
<path d="M85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999C82 36.343 83.3431 34.9999 85 34.9999Z" stroke="#00AFFF" stroke-width="2"/>
<rect x="23" y="11" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
<rect x="22" y="52" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

+15 -15
View File
@@ -1,19 +1,19 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="47" cy="36" r="34" fill="white"/>
<circle cx="47" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_1110_23775" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
<path d="M55.1358 38.5084C55.9608 38.4334 56.5688 37.7038 56.4938 36.8788C56.4188 36.0538 55.6892 35.4457 54.8642 35.5207L55.1358 38.5084ZM38.5 38.5146L38.6358 40.0084L55.1358 38.5084L55 37.0146L54.8642 35.5207L38.3642 37.0207L38.5 38.5146Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="39" cy="36" r="34" fill="white"/>
<circle cx="39" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
<path d="M33.8777 12.5216C35.4905 12.1798 37.1631 12 38.8777 12C50.2401 12 59.7582 19.8959 62.2445 30.5M32 59C34.1788 59.6506 36.4874 60 38.8777 60C48.9498 60 57.5728 53.7955 61.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_2_810" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<path d="M45.75 42.075C45.75 42.4462 45.4463 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1110_23775)">
<rect x="38" y="27" width="18" height="18" fill="#212121"/>
<g mask="url(#mask0_2_810)">
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
</g>
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
<path d="M55.5 39.4999C56.3284 39.4999 57 38.8283 57 37.9999C57 37.1715 56.3284 36.4999 55.5 36.4999L55.5 39.4999ZM41.5 37.9999L41.5 39.4999L55.5 39.4999L55.5 37.9999L55.5 36.4999L41.5 36.4999L41.5 37.9999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="23" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="22" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M63 34.9999C64.6569 34.9999 66 36.343 66 37.9999C66 39.6567 64.6569 40.9999 63 40.9999C61.3431 40.9999 60 39.6567 60 37.9999C60 36.343 61.3431 34.9999 63 34.9999Z" stroke="#00AFFF" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -1,19 +1,18 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="47" cy="36" r="34" fill="#1C1C1C"/>
<circle cx="47" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_1180_4965" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
<path d="M55.1358 38.5084C55.9608 38.4334 56.5688 37.7038 56.4938 36.8788C56.4188 36.0538 55.6892 35.4457 54.8642 35.5207L55.1358 38.5084ZM38.5 38.5146L38.6358 40.0084L55.1358 38.5084L55 37.0146L54.8642 35.5207L38.3642 37.0207L38.5 38.5146Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="39" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
<path d="M33.8777 12.5216C35.4905 12.1798 37.1631 12 38.8777 12C50.2401 12 59.7582 19.8959 62.2445 30.5M32 59C34.1788 59.6506 36.4874 60 38.8777 60C48.9498 60 57.5728 53.7955 61.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_2_810" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<path d="M45.75 42.075C45.75 42.4462 45.4463 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1180_4965)">
<rect x="38" y="27" width="18" height="18" fill="#00AFFF"/>
<g mask="url(#mask0_2_810)">
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
</g>
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
<path d="M55.5 39.4999C56.3284 39.4999 57 38.8283 57 37.9999C57 37.1715 56.3284 36.4999 55.5 36.4999L55.5 39.4999ZM41.5 37.9999L41.5 39.4999L55.5 39.4999L55.5 37.9999L55.5 36.4999L41.5 36.4999L41.5 37.9999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="23" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="22" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M63 34.9999C64.6569 34.9999 66 36.343 66 37.9999C66 39.6567 64.6569 40.9999 63 40.9999C61.3431 40.9999 60 39.6567 60 37.9999C60 36.343 61.3431 34.9999 63 34.9999Z" stroke="#00AFFF" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20250326.0"
version = "20250430.0"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"
+1 -1
View File
@@ -101,7 +101,7 @@ export class HaAuthFlow extends LitElement {
a.forgot-password {
color: var(--primary-color);
text-decoration: none;
font-size: var(--ha-font-size-s);
font-size: 0.875rem;
}
.space-between {
display: flex;
+4 -4
View File
@@ -93,8 +93,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
background-color: var(--primary-background-color, #fafafa);
}
p {
font-size: var(--ha-font-size-m);
line-height: var(--ha-line-height-normal);
font-size: 14px;
line-height: 20px;
}
.card-content {
background: var(
@@ -151,8 +151,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
margin-inline-start: initial;
}
h1 {
font-size: var(--ha-font-size-3xl);
font-weight: var(--ha-font-weight-normal);
font-size: 28px;
font-weight: 400;
margin-top: 16px;
margin-bottom: 16px;
}
+3 -3
View File
@@ -57,9 +57,9 @@ export class HaPickAuthProvider extends LitElement {
position: relative;
z-index: 1;
text-align: center;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-normal);
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
h3:before {
border-top: 1px solid var(--divider-color);
@@ -9,6 +9,8 @@ import type { LitElement } from "lit";
*/
export interface DragScrollControllerConfig {
selector: string;
enabled?: boolean;
trackScroll?: boolean;
}
export class DragScrollController implements ReactiveController {
@@ -22,38 +24,109 @@ export class DragScrollController implements ReactiveController {
public scrollLeft = 0;
public scrolledStart = false;
public scrolledEnd = false;
private _host: ReactiveControllerHost & LitElement;
private _selector: string;
private _scrollContainer?: HTMLElement | null;
private _enabled = true;
private _trackScroll = false;
public get enabled(): boolean {
return this._enabled;
}
public set enabled(value: boolean) {
if (value === this._enabled) {
return;
}
this._enabled = value;
if (this._enabled) {
this._attach();
} else {
this._detach();
}
this._host.requestUpdate();
}
constructor(
host: ReactiveControllerHost & LitElement,
{ selector }: DragScrollControllerConfig
{ selector, enabled, trackScroll }: DragScrollControllerConfig
) {
this._selector = selector;
this._host = host;
this._trackScroll = trackScroll ?? false;
this.enabled = enabled ?? true;
host.addController(this);
}
hostUpdated() {
if (this._scrollContainer) {
if (!this.enabled || this._scrollContainer) {
return;
}
this._attach();
}
hostDisconnected() {
this._detach();
}
private _attach() {
this._scrollContainer = this._host.renderRoot?.querySelector(
this._selector
);
if (this._scrollContainer) {
this._scrollContainer.addEventListener("mousedown", this._mouseDown);
if (this._trackScroll) {
this._scrollContainer.addEventListener("scroll", this._onScroll);
this.scrolledStart = this._scrollContainer.scrollLeft > 0;
this.scrolledEnd =
this._scrollContainer.scrollLeft + this._scrollContainer.offsetWidth <
this._scrollContainer.scrollWidth;
this._host.requestUpdate();
}
}
}
hostDisconnected() {
private _detach() {
window.removeEventListener("mousemove", this._mouseMove);
window.removeEventListener("mouseup", this._mouseUp);
if (this._scrollContainer) {
this._scrollContainer.removeEventListener("mousedown", this._mouseDown);
this._scrollContainer.removeEventListener("scroll", this._onScroll);
this._scrollContainer = undefined;
}
this.scrolled = false;
this.scrolling = false;
this.scrolledStart = false;
this.scrolledEnd = false;
this.mouseIsDown = false;
this.scrollStartX = 0;
this.scrollLeft = 0;
}
private _onScroll = (event: Event) => {
const oldScrolledStart = this.scrolledStart;
const oldScrolledEnd = this.scrolledEnd;
const container = event.currentTarget as HTMLElement;
this.scrolledStart = container.scrollLeft > 0;
this.scrolledEnd =
container.scrollLeft + container.offsetWidth < container.scrollWidth;
if (
this.scrolledStart !== oldScrolledStart ||
this.scrolledEnd !== oldScrolledEnd
) {
this._host.requestUpdate();
}
};
private _mouseDown = (event: MouseEvent) => {
const scrollContainer = this._scrollContainer;
+2 -10
View File
@@ -1,6 +1,5 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { ReactiveElement } from "lit";
import type { InternalPropertyDeclaration } from "lit/decorators";
import type { ReactiveElement } from "lit";
type Callback = (oldValue: any, newValue: any) => void;
@@ -108,7 +107,6 @@ export function storage(options: {
storage?: "localStorage" | "sessionStorage";
subscribe?: boolean;
state?: boolean;
stateOptions?: InternalPropertyDeclaration;
serializer?: (value: any) => any;
deserializer?: (value: any) => any;
}) {
@@ -174,7 +172,7 @@ export function storage(options: {
performUpdate.call(this);
};
if (options.state && options.subscribe) {
if (options.subscribe) {
const connectedCallback = proto.connectedCallback;
const disconnectedCallback = proto.disconnectedCallback;
@@ -192,12 +190,6 @@ export function storage(options: {
el.__unbsubLocalStorage = undefined;
};
}
if (options.state) {
ReactiveElement.createProperty(propertyKey, {
noAccessor: true,
...options.stateOptions,
});
}
const descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey);
let newDescriptor: PropertyDescriptor;
+1 -13
View File
@@ -1,10 +1,4 @@
import {
ReactiveElement,
type PropertyDeclaration,
type PropertyValues,
} from "lit";
import { shallowEqual } from "../util/shallow-equal";
import type { ReactiveElement, PropertyValues } from "lit";
/**
* Transform function type.
*/
@@ -23,7 +17,6 @@ type ReactiveTransformElement = ReactiveElement & {
export function transform<T, V>(config: {
transformer: Transformer<T, V>;
watch?: PropertyKey[];
propertyOptions?: PropertyDeclaration;
}) {
return <ElemClass extends ReactiveElement>(
proto: ElemClass,
@@ -84,11 +77,6 @@ export function transform<T, V>(config: {
curWatch.add(propertyKey);
});
}
ReactiveElement.createProperty(propertyKey, {
noAccessor: true,
hasChanged: (v: any, o: any) => !shallowEqual(v, o),
...config.propertyOptions,
});
const descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey);
let newDescriptor: PropertyDescriptor;
+36
View File
@@ -0,0 +1,36 @@
export const canOverrideAlphanumericInput = (composedPath: EventTarget[]) => {
if (
composedPath.some(
(el) =>
"tagName" in el &&
(el.tagName === "HA-MENU" || el.tagName === "HA-CODE-EDITOR")
)
) {
return false;
}
const el = composedPath[0] as Element;
if (el.tagName === "TEXTAREA") {
return false;
}
if (el.parentElement?.tagName === "HA-SELECT") {
return false;
}
if (el.tagName !== "INPUT") {
return true;
}
switch ((el as HTMLInputElement).type) {
case "button":
case "checkbox":
case "hidden":
case "radio":
case "range":
return true;
default:
return false;
}
};
+4 -23
View File
@@ -6,34 +6,15 @@ interface AreaContext {
area: AreaRegistryEntry | null;
floor: FloorRegistryEntry | null;
}
/**
* Retrieves the context of a specific area, including its associated area registry entry
* and floor registry entry, if available.
*
* @param areaId - The unique identifier of the area to retrieve context for.
* @param hass - The Home Assistant instance containing area and floor registry data.
* @returns An object containing the area registry entry and the associated floor registry entry,
* or `null` values if the area or floor is not found.
*/
export const getAreaContext = (
areaId: string,
area: AreaRegistryEntry,
hass: HomeAssistant
): AreaContext => {
const area = (hass.areas[areaId] as AreaRegistryEntry | undefined) || null;
if (!area) {
return {
area: null,
floor: null,
};
}
const floorId = area?.floor_id;
const floor = floorId ? hass.floors[floorId] : null;
const floorId = area.floor_id;
const floor = floorId ? hass.floors[floorId] : undefined;
return {
area: area,
floor: floor,
floor: floor || null,
};
};
@@ -0,0 +1,26 @@
import type { AreaRegistryEntry } from "../../../data/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device_registry";
import type { FloorRegistryEntry } from "../../../data/floor_registry";
import type { HomeAssistant } from "../../../types";
interface DeviceContext {
device: DeviceRegistryEntry;
area: AreaRegistryEntry | null;
floor: FloorRegistryEntry | null;
}
export const getDeviceContext = (
device: DeviceRegistryEntry,
hass: HomeAssistant
): DeviceContext => {
const areaId = device.area_id;
const area = areaId ? hass.areas[areaId] : undefined;
const floorId = area?.floor_id;
const floor = floorId ? hass.floors[floorId] : undefined;
return {
device: device,
area: area || null,
floor: floor || null,
};
};
+29 -26
View File
@@ -1,6 +1,11 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../../../data/entity_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
ExtEntityRegistryEntry,
} from "../../../data/entity_registry";
import type { FloorRegistryEntry } from "../../../data/floor_registry";
import type { HomeAssistant } from "../../../types";
@@ -11,27 +16,15 @@ interface EntityContext {
floor: FloorRegistryEntry | null;
}
/**
* Retrieves the context of an entity, including its associated device, area, and floor.
*
* @param entityId - The unique identifier of the entity to retrieve the context for.
* @param hass - The Home Assistant object containing the registry data for entities, devices, areas, and floors.
* @returns An object containing the entity, its associated device, area, and floor, or `null` for each if not found.
*
* The returned `EntityContext` object includes:
* - `entity`: The entity registry entry, or `null` if the entity is not found.
* - `device`: The device registry entry associated with the entity, or `null` if not found.
* - `area`: The area registry entry associated with the entity or device, or `null` if not found.
* - `floor`: The floor registry entry associated with the area, or `null` if not found.
*/
export const getEntityContext = (
entityId: string,
stateObj: HassEntity,
hass: HomeAssistant
): EntityContext => {
const entity =
(hass.entities[entityId] as EntityRegistryDisplayEntry | undefined) || null;
const entry = hass.entities[stateObj.entity_id] as
| EntityRegistryDisplayEntry
| undefined;
if (!entity) {
if (!entry) {
return {
entity: null,
device: null,
@@ -39,18 +32,28 @@ export const getEntityContext = (
floor: null,
};
}
return getEntityEntryContext(entry, hass);
};
const deviceId = entity?.device_id;
const device = deviceId ? hass.devices[deviceId] : null;
const areaId = entity?.area_id || device?.area_id;
const area = areaId ? hass.areas[areaId] : null;
export const getEntityEntryContext = (
entry:
| EntityRegistryDisplayEntry
| EntityRegistryEntry
| ExtEntityRegistryEntry,
hass: HomeAssistant
): EntityContext => {
const entity = hass.entities[entry.entity_id];
const deviceId = entry?.device_id;
const device = deviceId ? hass.devices[deviceId] : undefined;
const areaId = entry?.area_id || device?.area_id;
const area = areaId ? hass.areas[areaId] : undefined;
const floorId = area?.floor_id;
const floor = floorId ? hass.floors[floorId] : null;
const floor = floorId ? hass.floors[floorId] : undefined;
return {
entity: entity,
device: device,
area: area,
floor: floor,
device: device || null,
area: area || null,
floor: floor || null,
};
};
+1 -1
View File
@@ -60,7 +60,7 @@ export const generateEntityFilter = (
}
}
const { area, floor, device, entity } = getEntityContext(entityId, hass);
const { area, floor, device, entity } = getEntityContext(stateObj, hass);
if (entity && entity.hidden) {
return false;
-18
View File
@@ -1,18 +0,0 @@
import type { AreaRegistryEntry } from "../../data/area_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import type { HomeAssistant } from "../../types";
interface AreaContext {
floor: FloorRegistryEntry | null;
}
export const getAreaContext = (
area: AreaRegistryEntry,
hass: HomeAssistant
): AreaContext => {
const floorId = area.floor_id;
const floor = floorId ? hass.floors[floorId] : null;
return {
floor: floor,
};
};
-24
View File
@@ -1,24 +0,0 @@
import type { AreaRegistryEntry } from "../../data/area_registry";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import type { HomeAssistant } from "../../types";
interface DeviceContext {
area: AreaRegistryEntry | null;
floor: FloorRegistryEntry | null;
}
export const getDeviceContext = (
device: DeviceRegistryEntry,
hass: HomeAssistant
): DeviceContext => {
const areaId = device.area_id;
const area = areaId ? hass.areas[areaId] : null;
const floorId = area?.floor_id;
const floor = floorId ? hass.floors[floorId] : null;
return {
area: area,
floor: floor,
};
};
-55
View File
@@ -1,55 +0,0 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { AreaRegistryEntry } from "../../data/area_registry";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
ExtEntityRegistryEntry,
} from "../../data/entity_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import type { HomeAssistant } from "../../types";
interface EntityContext {
device: DeviceRegistryEntry | null;
area: AreaRegistryEntry | null;
floor: FloorRegistryEntry | null;
}
export const getEntityContext = (
stateObj: HassEntity,
hass: HomeAssistant
): EntityContext => {
const entry = hass.entities[stateObj.entity_id] as
| EntityRegistryDisplayEntry
| undefined;
if (!entry) {
return {
device: null,
area: null,
floor: null,
};
}
return getEntityEntryContext(entry, hass);
};
export const getEntityEntryContext = (
entry:
| EntityRegistryDisplayEntry
| EntityRegistryEntry
| ExtEntityRegistryEntry,
hass: HomeAssistant
): EntityContext => {
const deviceId = entry?.device_id;
const device = deviceId ? hass.devices[deviceId] : null;
const areaId = entry?.area_id || device?.area_id;
const area = areaId ? hass.areas[areaId] : null;
const floorId = area?.floor_id;
const floor = floorId ? hass.floors[floorId] : null;
return {
device: device,
area: area,
floor: floor,
};
};
+1 -1
View File
@@ -96,7 +96,7 @@ const customGenerator = (colors: Swatch[]) => {
// eslint-disable-next-line no-console
console.log(
"%cPicked colors",
`color: ${foregroundColor}; background-color: ${backgroundColor.hex}; font-weight: var(--ha-font-weight-bold); padding: 16px;`
`color: ${foregroundColor}; background-color: ${backgroundColor.hex}; font-weight: bold; padding: 16px;`
);
colors.forEach((color) => logColor(color));
// eslint-disable-next-line no-console
+25 -5
View File
@@ -600,12 +600,32 @@ export class HaChartBase extends LitElement {
}
private _getSeries() {
if (!Array.isArray(this.data)) {
return this.data;
}
return this.data.filter(
const series = ensureArray(this.data).filter(
(d) => !this._hiddenDatasets.has(String(d.name ?? d.id))
);
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
| YAXisOption
| undefined;
if (yAxis?.type === "log") {
// set <=0 values to null so they render as gaps on a log graph
return series.map((d) =>
d.type === "line"
? {
...d,
data: d.data?.map((v) =>
Array.isArray(v)
? [
v[0],
typeof v[1] !== "number" || v[1] > 0 ? v[1] : null,
...v.slice(2),
]
: v
),
}
: d
);
}
return series;
}
private _getDefaultHeight() {
@@ -719,7 +739,7 @@ export class HaChartBase extends LitElement {
max-height: 60%;
overflow-y: auto;
padding: 12px 0 0;
font-size: var(--ha-font-size-s);
font-size: 12px;
color: var(--primary-text-color);
}
.chart-legend ul {
+5 -1
View File
@@ -106,6 +106,10 @@ export class HaSankeyChart extends LitElement {
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))];
const depthMap = new Map<number, number>();
indexes.sort().forEach((index, i) => {
depthMap.set(index, i);
});
const links = this._processLinks(filteredNodes, data.links);
const sectionWidth = width / indexes.length;
const labelSpace = sectionWidth - NODE_SIZE - LABEL_DISTANCE;
@@ -119,7 +123,7 @@ export class HaSankeyChart extends LitElement {
itemStyle: {
color: node.color,
},
depth: node.index,
depth: depthMap.get(node.index),
})),
links,
draggable: false,
+1 -1
View File
@@ -293,7 +293,7 @@ export class StateHistoryCharts extends LitElement {
.info {
text-align: center;
line-height: var(--ha-line-height-expanded);
line-height: 60px;
color: var(--secondary-text-color);
}
+1 -1
View File
@@ -634,7 +634,7 @@ export class StatisticsChart extends LitElement {
}
.info {
text-align: center;
line-height: var(--ha-line-height-expanded);
line-height: 60px;
color: var(--secondary-text-color);
}
`;
+11 -11
View File
@@ -603,7 +603,7 @@ export class HaDataTable extends LitElement {
.map(
([key2, column2], i) =>
html`${i !== 0
? " "
? " · "
: nothing}${column2.template
? column2.template(row)
: row[key2]}`
@@ -931,9 +931,9 @@ export class HaDataTable extends LitElement {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-size: var(--ha-font-size-s);
line-height: var(--ha-line-height-condensed);
font-weight: var(--ha-font-weight-normal);
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
letter-spacing: 0.0178571429em;
text-decoration: inherit;
text-transform: inherit;
@@ -1051,9 +1051,9 @@ export class HaDataTable extends LitElement {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-size: var(--ha-font-size-s);
line-height: var(--ha-line-height-normal);
font-weight: var(--ha-font-weight-normal);
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
letter-spacing: 0.0178571429em;
text-decoration: inherit;
text-transform: inherit;
@@ -1173,9 +1173,9 @@ export class HaDataTable extends LitElement {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-size: var(--ha-font-size-s);
line-height: var(--ha-line-height-normal);
font-weight: var(--ha-font-weight-semibold);
font-size: 0.875rem;
line-height: 1.375rem;
font-weight: 500;
letter-spacing: 0.0071428571em;
text-decoration: inherit;
text-transform: inherit;
@@ -1199,7 +1199,7 @@ export class HaDataTable extends LitElement {
padding-inline-start: 12px;
padding-inline-end: initial;
width: 100%;
font-weight: var(--ha-font-weight-semibold);
font-weight: 500;
display: flex;
align-items: center;
cursor: pointer;
+14 -11
View File
@@ -4,11 +4,11 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import type { HaEntityComboBoxEntityFilterFunc } from "./ha-entity-combo-box";
import "./ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
@customElement("ha-entities-picker")
class HaEntitiesPickerLight extends LitElement {
class HaEntitiesPicker extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Array }) public value?: string[];
@@ -17,6 +17,10 @@ class HaEntitiesPickerLight extends LitElement {
@property({ type: Boolean }) public required = false;
@property() public label?: string;
@property() public placeholder?: string;
@property() public helper?: string;
/**
@@ -67,13 +71,8 @@ class HaEntitiesPickerLight extends LitElement {
@property({ type: Array, attribute: "exclude-entities" })
public excludeEntities?: string[];
@property({ attribute: "picked-entity-label" })
public pickedEntityLabel?: string;
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
@property({ attribute: false })
public entityFilter?: HaEntityPickerEntityFilterFunc;
public entityFilter?: HaEntityComboBoxEntityFilterFunc;
@property({ attribute: false, type: Array }) public createDomains?: string[];
@@ -84,6 +83,7 @@ class HaEntitiesPickerLight extends LitElement {
const currentEntities = this._currentEntities;
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
${currentEntities.map(
(entityId) => html`
<div>
@@ -99,7 +99,6 @@ class HaEntitiesPickerLight extends LitElement {
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this.entityFilter}
.value=${entityId}
.label=${this.pickedEntityLabel}
.disabled=${this.disabled}
.createDomains=${this.createDomains}
@value-changed=${this._entityChanged}
@@ -121,7 +120,7 @@ class HaEntitiesPickerLight extends LitElement {
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this.entityFilter}
.label=${this.pickEntityLabel}
.placeholder=${this.placeholder}
.helper=${this.helper}
.disabled=${this.disabled}
.createDomains=${this.createDomains}
@@ -198,11 +197,15 @@ class HaEntitiesPickerLight extends LitElement {
div {
margin-top: 8px;
}
label {
display: block;
margin: 0 0 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-entities-picker": HaEntitiesPickerLight;
"ha-entities-picker": HaEntitiesPicker;
}
}
@@ -73,16 +73,20 @@ class HaEntityAttributePicker extends LitElement {
return nothing;
}
const stateObj = this.hass.states[this.entityId!] as HassEntity | undefined;
return html`
<ha-combo-box
.hass=${this.hass}
.value=${this.value
? computeAttributeNameDisplay(
this.hass.localize,
this.hass.states[this.entityId!],
this.hass.entities,
this.value
)
? stateObj
? computeAttributeNameDisplay(
this.hass.localize,
stateObj,
this.hass.entities,
this.value
)
: this.value
: ""}
.autofocus=${this.autofocus}
.label=${this.label ??
@@ -0,0 +1,510 @@
import { mdiMagnify, mdiPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import Fuse from "fuse.js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { computeRTL } from "../../common/util/compute_rtl";
import { domainToName } from "../../data/integration";
import type { HelperDomain } from "../../panels/config/helpers/const";
import { isHelperDomain } from "../../panels/config/helpers/const";
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
import { HaFuse } from "../../resources/fuse";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box-item";
import "../ha-icon-button";
import "../ha-svg-icon";
import "./state-badge";
interface EntityComboBoxItem {
// Force empty label to always display empty value by default in the search field
id: string;
label: "";
primary: string;
secondary?: string;
domain_name?: string;
search_labels?: string[];
sorting_label?: string;
icon_path?: string;
stateObj?: HassEntity;
}
export type HaEntityComboBoxEntityFilterFunc = (entity: HassEntity) => boolean;
const CREATE_ID = "___create-new-entity___";
const NO_ENTITIES_ID = "___no-entities___";
@customElement("ha-entity-combo-box")
export class HaEntityComboBox extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@property() public label?: string;
@property() public value?: string;
@property() public helper?: string;
@property({ attribute: false, type: Array }) public createDomains?: string[];
/**
* Show entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
/**
* Show only entities with these unit of measuments.
* @type {Array}
* @attr include-unit-of-measurement
*/
@property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[];
/**
* List of allowed entities to show.
* @type {Array}
* @attr include-entities
*/
@property({ type: Array, attribute: "include-entities" })
public includeEntities?: string[];
/**
* List of entities to be excluded.
* @type {Array}
* @attr exclude-entities
*/
@property({ type: Array, attribute: "exclude-entities" })
public excludeEntities?: string[];
@property({ attribute: false })
public entityFilter?: HaEntityComboBoxEntityFilterFunc;
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@state() private _opened = false;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
public async open() {
await this.updateComplete;
await this.comboBox?.open();
}
public async focus() {
await this.updateComplete;
await this.comboBox?.focus();
}
private _initialItems = false;
private _items: EntityComboBoxItem[] = [];
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.hass.loadBackendTranslation("title");
}
private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = (
item,
{ index }
) => {
const showEntityId = this.hass.userData?.showEntityIdPicker;
return html`
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
${item.icon_path
? html`
<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>
`
: html`
<state-badge
slot="start"
.stateObj=${item.stateObj}
.hass=${this.hass}
></state-badge>
`}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.stateObj && showEntityId
? html`
<span slot="supporting-text" class="code">
${item.stateObj.entity_id}
</span>
`
: nothing}
${item.domain_name && !showEntityId
? html`
<div slot="trailing-supporting-text">${item.domain_name}</div>
`
: nothing}
</ha-combo-box-item>
`;
};
private _getItems = memoizeOne(
(
_opened: boolean,
hass: this["hass"],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
includeEntities: this["includeEntities"],
excludeEntities: this["excludeEntities"],
createDomains: this["createDomains"]
): EntityComboBoxItem[] => {
let items: EntityComboBoxItem[] = [];
let entityIds = Object.keys(hass.states);
const createItems = createDomains?.length
? createDomains.map((domain) => {
const primary = hass.localize(
"ui.components.entity.entity-picker.create_helper",
{
domain: isHelperDomain(domain)
? hass.localize(
`ui.panel.config.helpers.types.${domain as HelperDomain}`
)
: domainToName(hass.localize, domain),
}
);
return {
id: CREATE_ID + domain,
label: "",
primary: primary,
secondary: this.hass.localize(
"ui.components.entity.entity-picker.new_entity"
),
icon_path: mdiPlus,
} satisfies EntityComboBoxItem;
})
: [];
if (!entityIds.length) {
return [
{
id: NO_ENTITIES_ID,
label: "",
primary: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
icon_path: mdiMagnify,
},
...createItems,
];
}
if (includeEntities) {
entityIds = entityIds.filter((entityId) =>
includeEntities.includes(entityId)
);
}
if (excludeEntities) {
entityIds = entityIds.filter(
(entityId) => !excludeEntities.includes(entityId)
);
}
if (includeDomains) {
entityIds = entityIds.filter((eid) =>
includeDomains.includes(computeDomain(eid))
);
}
if (excludeDomains) {
entityIds = entityIds.filter(
(eid) => !excludeDomains.includes(computeDomain(eid))
);
}
const isRTL = computeRTL(this.hass);
items = entityIds
.map<EntityComboBoxItem>((entityId) => {
const stateObj = hass!.states[entityId];
const { area, device } = getEntityContext(stateObj, hass);
const friendlyName = computeStateName(stateObj); // Keep this for search
const entityName = computeEntityName(stateObj, hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const domainName = domainToName(
this.hass.localize,
computeDomain(entityId)
);
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
return {
id: entityId,
label: "",
primary: primary,
secondary: secondary,
domain_name: domainName,
sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
search_labels: [
entityName,
deviceName,
areaName,
domainName,
friendlyName,
entityId,
].filter(Boolean) as string[],
stateObj: stateObj,
};
})
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.sorting_label!,
entityB.sorting_label!,
this.hass.locale.language
)
);
if (includeDeviceClasses) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj?.attributes.device_class &&
includeDeviceClasses.includes(
item.stateObj.attributes.device_class
))
);
}
if (includeUnitOfMeasurement) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj?.attributes.unit_of_measurement &&
includeUnitOfMeasurement.includes(
item.stateObj.attributes.unit_of_measurement
))
);
}
if (entityFilter) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj && entityFilter!(item.stateObj))
);
}
if (!items.length) {
return [
{
id: NO_ENTITIES_ID,
label: "",
primary: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
icon_path: mdiMagnify,
},
...createItems,
];
}
if (createItems?.length) {
items.push(...createItems);
}
return items;
}
);
protected shouldUpdate(changedProps: PropertyValues) {
if (
changedProps.has("value") ||
changedProps.has("label") ||
changedProps.has("disabled")
) {
return true;
}
return !(!changedProps.has("_opened") && this._opened);
}
public willUpdate(changedProps: PropertyValues) {
if (!this._initialItems || (changedProps.has("_opened") && this._opened)) {
this._items = this._getItems(
this._opened,
this.hass,
this.includeDomains,
this.excludeDomains,
this.entityFilter,
this.includeDeviceClasses,
this.includeUnitOfMeasurement,
this.includeEntities,
this.excludeEntities,
this.createDomains
);
if (this._initialItems) {
this.comboBox.filteredItems = this._items;
}
this._initialItems = true;
}
if (changedProps.has("createDomains") && this.createDomains?.length) {
this.hass.loadFragmentTranslation("config");
}
}
protected render(): TemplateResult {
return html`
<ha-combo-box
item-value-path="id"
.hass=${this.hass}
.value=${this._value}
.label=${this.label === undefined
? this.hass.localize("ui.components.entity.entity-picker.entity")
: this.label}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._items}
.renderer=${this._rowRenderer}
.required=${this.required}
.disabled=${this.disabled}
.hideClearIcon=${this.hideClearIcon}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
>
</ha-combo-box>
`;
}
private get _value() {
return this.value || "";
}
private _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
ev.stopPropagation();
// Clear the input field to prevent showing the old value next time
this.comboBox.setTextFieldValue("");
const newValue = ev.detail.value?.trim();
if (newValue && newValue.startsWith(CREATE_ID)) {
const domain = newValue.substring(CREATE_ID.length);
showHelperDetailDialog(this, {
domain,
dialogClosedCallback: (item) => {
if (item.entityId) this._setValue(item.entityId);
},
});
return;
}
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _fuseIndex = memoizeOne((states: EntityComboBoxItem[]) =>
Fuse.createIndex(["search_labels"], states)
);
private _filterChanged(ev: CustomEvent): void {
if (!this._opened) return;
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.trim().toLowerCase() as string;
const index = this._fuseIndex(this._items);
const fuse = new HaFuse(this._items, {}, index);
const results = fuse.multiTermsSearch(filterString);
if (results) {
if (results.length === 0) {
target.filteredItems = [
{
id: NO_ENTITIES_ID,
label: "",
primary: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
icon_path: mdiMagnify,
},
] as EntityComboBoxItem[];
} else {
target.filteredItems = results.map((result) => result.item);
}
} else {
target.filteredItems = this._items;
}
}
private _setValue(value: string | undefined) {
if (!value || !isValidEntityId(value)) {
return;
}
setTimeout(() => {
fireEvent(this, "value-changed", { value });
}, 0);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-entity-combo-box": HaEntityComboBox;
}
}
+215 -407
View File
@@ -1,77 +1,27 @@
import { mdiMagnify, mdiPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import Fuse from "fuse.js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { mdiClose, mdiMenuDown, mdiShape } from "@mdi/js";
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityContext } from "../../common/entity/get_entity_context";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { computeRTL } from "../../common/util/compute_rtl";
import { domainToName } from "../../data/integration";
import type { HelperDomain } from "../../panels/config/helpers/const";
import { isHelperDomain } from "../../panels/config/helpers/const";
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
import { HaFuse } from "../../resources/fuse";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import { debounce } from "../../common/util/debounce";
import type { HomeAssistant } from "../../types";
import "../ha-combo-box-item";
import "../ha-icon-button";
import type { HaMdListItem } from "../ha-md-list-item";
import "../ha-svg-icon";
import "./ha-entity-combo-box";
import type {
HaEntityComboBox,
HaEntityComboBoxEntityFilterFunc,
} from "./ha-entity-combo-box";
import "./state-badge";
const FAKE_ENTITY: HassEntity = {
entity_id: "",
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
attributes: {},
};
interface EntityPickerItem extends HassEntity {
label: string;
primary: string;
secondary?: string;
translated_domain?: string;
show_entity_id?: boolean;
entity_name?: string;
area_name?: string;
device_name?: string;
friendly_name?: string;
sorting_label?: string;
icon_path?: string;
}
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
const CREATE_ID = "___create-new-entity___";
const DOMAIN_STYLE = styleMap({
fontSize: "12px",
fontWeight: "400",
lineHeight: "18px",
alignSelf: "flex-end",
maxWidth: "30%",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
});
const ENTITY_ID_STYLE = styleMap({
fontFamily: "var(--code-font-family, monospace)",
fontSize: "11px",
});
@customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -92,6 +42,8 @@ export class HaEntityPicker extends LitElement {
@property() public helper?: string;
@property() public placeholder?: string;
@property({ attribute: false, type: Array }) public createDomains?: string[];
/**
@@ -143,381 +95,237 @@ export class HaEntityPicker extends LitElement {
public excludeEntities?: string[];
@property({ attribute: false })
public entityFilter?: HaEntityPickerEntityFilterFunc;
public entityFilter?: HaEntityComboBoxEntityFilterFunc;
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ attribute: "item-label-path" }) public itemLabelPath = "label";
@query("#anchor") private _anchor?: HaMdListItem;
@query("#input") private _input?: HaEntityComboBox;
@state() private _opened = false;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _renderContent() {
const entityId = this.value || "";
public async open() {
await this.updateComplete;
await this.comboBox?.open();
}
public async focus() {
await this.updateComplete;
await this.comboBox?.focus();
}
private _initialItems = false;
private _items: EntityPickerItem[] = [];
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.hass.loadBackendTranslation("title");
}
private _rowRenderer: ComboBoxLitRenderer<EntityPickerItem> = (
item,
{ index }
) => html`
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
${item.icon_path
? html`<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>`
: html`
<state-badge
slot="start"
.stateObj=${item}
.hass=${this.hass}
></state-badge>
`}
<span slot="headline">${item.primary} </span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.entity_id && item.show_entity_id
? html`<span slot="supporting-text" style=${ENTITY_ID_STYLE}
>${item.entity_id}</span
>`
: nothing}
${item.translated_domain && !item.show_entity_id
? html`<div slot="trailing-supporting-text" style=${DOMAIN_STYLE}>
${item.translated_domain}
</div>`
: nothing}
</ha-combo-box-item>
`;
private _getItems = memoizeOne(
(
_opened: boolean,
hass: this["hass"],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
includeEntities: this["includeEntities"],
excludeEntities: this["excludeEntities"],
createDomains: this["createDomains"]
): EntityPickerItem[] => {
let states: EntityPickerItem[] = [];
if (!hass) {
return [];
}
let entityIds = Object.keys(hass.states);
const createItems = createDomains?.length
? createDomains.map((domain) => {
const primary = hass.localize(
"ui.components.entity.entity-picker.create_helper",
{
domain: isHelperDomain(domain)
? hass.localize(
`ui.panel.config.helpers.types.${domain as HelperDomain}`
)
: domainToName(hass.localize, domain),
}
);
return {
...FAKE_ENTITY,
entity_id: CREATE_ID + domain,
primary: primary,
label: primary,
secondary: this.hass.localize(
"ui.components.entity.entity-picker.new_entity"
),
icon_path: mdiPlus,
};
})
: [];
if (!entityIds.length) {
return [
{
...FAKE_ENTITY,
primary: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
label: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
icon_path: mdiMagnify,
},
...createItems,
];
}
if (includeEntities) {
entityIds = entityIds.filter((entityId) =>
includeEntities.includes(entityId)
);
}
if (excludeEntities) {
entityIds = entityIds.filter(
(entityId) => !excludeEntities.includes(entityId)
);
}
if (includeDomains) {
entityIds = entityIds.filter((eid) =>
includeDomains.includes(computeDomain(eid))
);
}
if (excludeDomains) {
entityIds = entityIds.filter(
(eid) => !excludeDomains.includes(computeDomain(eid))
);
}
const isRTL = computeRTL(this.hass);
states = entityIds
.map<EntityPickerItem>((entityId) => {
const stateObj = hass!.states[entityId];
const { area, device } = getEntityContext(stateObj, hass);
const friendlyName = computeStateName(stateObj); // Keep this for search
const entityName = computeEntityName(stateObj, hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const translatedDomain = domainToName(
this.hass.localize,
computeDomain(entityId)
);
return {
...hass!.states[entityId],
primary: primary,
secondary:
secondary ||
this.hass.localize("ui.components.device-picker.no_area"),
label: friendlyName,
translated_domain: translatedDomain,
sorting_label: [deviceName, entityName].filter(Boolean).join("-"),
entity_name: entityName || deviceName,
area_name: areaName,
device_name: deviceName,
friendly_name: friendlyName,
show_entity_id: hass.userData?.showEntityIdPicker,
};
})
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.sorting_label!,
entityB.sorting_label!,
this.hass.locale.language
)
);
if (includeDeviceClasses) {
states = states.filter(
(stateObj) =>
// We always want to include the entity of the current value
stateObj.entity_id === this.value ||
(stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class))
);
}
if (includeUnitOfMeasurement) {
states = states.filter(
(stateObj) =>
// We always want to include the entity of the current value
stateObj.entity_id === this.value ||
(stateObj.attributes.unit_of_measurement &&
includeUnitOfMeasurement.includes(
stateObj.attributes.unit_of_measurement
))
);
}
if (entityFilter) {
states = states.filter(
(stateObj) =>
// We always want to include the entity of the current value
stateObj.entity_id === this.value || entityFilter!(stateObj)
);
}
if (!states.length) {
return [
{
...FAKE_ENTITY,
primary: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
label: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
icon_path: mdiMagnify,
},
...createItems,
];
}
if (createItems?.length) {
states.push(...createItems);
}
return states;
}
);
protected shouldUpdate(changedProps: PropertyValues) {
if (
changedProps.has("value") ||
changedProps.has("label") ||
changedProps.has("disabled")
) {
return true;
}
return !(!changedProps.has("_opened") && this._opened);
}
public willUpdate(changedProps: PropertyValues) {
if (!this._initialItems || (changedProps.has("_opened") && this._opened)) {
this._items = this._getItems(
this._opened,
this.hass,
this.includeDomains,
this.excludeDomains,
this.entityFilter,
this.includeDeviceClasses,
this.includeUnitOfMeasurement,
this.includeEntities,
this.excludeEntities,
this.createDomains
);
if (this._initialItems) {
this.comboBox.filteredItems = this._items;
}
this._initialItems = true;
if (!this.value) {
return html`
<span slot="headline" class="placeholder"
>${this.placeholder ??
this.hass.localize(
"ui.components.entity.entity-picker.placeholder"
)}</span
>
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
`;
}
if (changedProps.has("createDomains") && this.createDomains?.length) {
this.hass.loadFragmentTranslation("config");
const stateObj = this.hass.states[entityId];
const showClearIcon =
!this.required && !this.disabled && !this.hideClearIcon;
if (!stateObj) {
return html`
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
<span slot="headline">${entityId}</span>
${showClearIcon
? html`<ha-icon-button
class="clear"
slot="end"
@click=${this._clear}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
`;
}
}
const { area, device } = getEntityContext(stateObj, this.hass);
const entityName = computeEntityName(stateObj, this.hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const isRTL = computeRTL(this.hass);
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
protected render(): TemplateResult {
return html`
<ha-combo-box
item-value-path="entity_id"
.itemLabelPath=${this.itemLabelPath}
<state-badge
.hass=${this.hass}
.value=${this._value}
.label=${this.label === undefined
? this.hass.localize("ui.components.entity.entity-picker.entity")
: this.label}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._items}
.renderer=${this._rowRenderer}
.required=${this.required}
.disabled=${this.disabled}
.hideClearIcon=${this.hideClearIcon}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
>
</ha-combo-box>
.stateObj=${stateObj}
slot="start"
></state-badge>
<span slot="headline">${primary}</span>
<span slot="supporting-text">${secondary}</span>
${showClearIcon
? html`<ha-icon-button
class="clear"
slot="end"
@click=${this._clear}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
`;
}
private get _value() {
return this.value || "";
protected render() {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<div class="container">
${!this._opened
? html`<ha-combo-box-item
.disabled=${this.disabled}
id="anchor"
type="button"
compact
@click=${this._showPicker}
>
${this._renderContent()}
</ha-combo-box-item>`
: html`<ha-entity-combo-box
id="input"
.hass=${this.hass}
.autofocus=${this.autofocus}
.allowCustomEntity=${this.allowCustomEntity}
.label=${this.hass.localize("ui.common.search")}
.value=${this.value}
.createDomains=${this.createDomains}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.includeEntities=${this.includeEntities}
.excludeEntities=${this.excludeEntities}
.entityFilter=${this.entityFilter}
hide-clear-icon
@opened-changed=${this._debounceOpenedChanged}
@input=${stopPropagation}
></ha-entity-combo-box>`}
${this._renderHelper()}
</div>
`;
}
private _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
private _renderHelper() {
return this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: nothing;
}
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
ev.stopPropagation();
const newValue = ev.detail.value?.trim();
private _clear(e) {
e.stopPropagation();
this.value = undefined;
fireEvent(this, "value-changed", { value: undefined });
fireEvent(this, "change");
}
if (newValue && newValue.startsWith(CREATE_ID)) {
const domain = newValue.substring(CREATE_ID.length);
showHelperDetailDialog(this, {
domain,
dialogClosedCallback: (item) => {
if (item.entityId) this._setValue(item.entityId);
},
});
private async _showPicker() {
if (this.disabled) {
return;
}
if (newValue !== this._value) {
this._setValue(newValue);
}
this._opened = true;
await this.updateComplete;
this._input?.focus();
this._input?.open();
}
private _fuseIndex = memoizeOne((states: EntityPickerItem[]) =>
Fuse.createIndex(
[
"entity_name",
"device_name",
"area_name",
"translated_domain",
"friendly_name", // for backwards compatibility
"entity_id", // for technical search
],
states
)
// Multiple calls to _openedChanged can be triggered in quick succession
// when the menu is opened
private _debounceOpenedChanged = debounce(
(ev) => this._openedChanged(ev),
10
);
private _filterChanged(ev: CustomEvent): void {
if (!this._opened) return;
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.trim().toLowerCase() as string;
const index = this._fuseIndex(this._items);
const fuse = new HaFuse(this._items, {}, index);
const results = fuse.multiTermsSearch(filterString);
if (results) {
target.filteredItems = results.map((result) => result.item);
} else {
target.filteredItems = this._items;
private async _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
const opened = ev.detail.value;
if (this._opened && !opened) {
this._opened = false;
await this.updateComplete;
this._anchor?.focus();
}
}
private _setValue(value: string | undefined) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
static get styles(): CSSResultGroup {
return [
css`
mwc-menu-surface {
--mdc-menu-min-width: 100%;
}
.container {
position: relative;
display: block;
}
ha-combo-box-item {
background-color: var(--mdc-text-field-fill-color, whitesmoke);
border-radius: 4px;
border-end-end-radius: 0;
border-end-start-radius: 0;
--md-list-item-one-line-container-height: 56px;
--md-list-item-two-line-container-height: 56px;
--md-list-item-top-space: 8px;
--md-list-item-bottom-space: 8px;
--md-list-item-leading-space: 8px;
--md-list-item-trailing-space: 8px;
--ha-md-list-item-gap: 8px;
/* Remove the default focus ring */
--md-focus-ring-width: 0px;
--md-focus-ring-duration: 0s;
}
/* Add Similar focus style as the text field */
ha-combo-box-item:after {
display: block;
content: "";
position: absolute;
pointer-events: none;
bottom: 0;
left: 0;
right: 0;
height: 1px;
width: 100%;
background-color: var(
--mdc-text-field-idle-line-color,
rgba(0, 0, 0, 0.42)
);
transform:
height 180ms ease-in-out,
background-color 180ms ease-in-out;
}
ha-combo-box-item:focus:after {
height: 2px;
background-color: var(--mdc-theme-primary);
}
ha-combo-box-item ha-svg-icon[slot="start"] {
margin: 0 4px;
}
.clear {
margin: 0 -8px;
--mdc-icon-button-size: 32px;
--mdc-icon-size: 20px;
}
.edit {
--mdc-icon-size: 20px;
width: 32px;
}
label {
display: block;
margin: 0 0 8px;
}
.placeholder {
color: var(--secondary-text-color);
padding: 0 8px;
}
`,
];
}
}
@@ -0,0 +1,481 @@
import { mdiChartLine, mdiHelpCircle, mdiShape } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import Fuse from "fuse.js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { computeRTL } from "../../common/util/compute_rtl";
import { domainToName } from "../../data/integration";
import type { StatisticsMetaData } from "../../data/recorder";
import { getStatisticIds, getStatisticLabel } from "../../data/recorder";
import { HaFuse } from "../../resources/fuse";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box-item";
import "../ha-svg-icon";
import "./state-badge";
import { documentationUrl } from "../../util/documentation-url";
type StatisticItemType = "entity" | "external" | "no_state";
interface StatisticItem {
// Force empty label to always display empty value by default in the search field
id: string;
statistic_id?: string;
label: "";
primary: string;
secondary?: string;
search_labels?: string[];
sorting_label?: string;
icon_path?: string;
type?: StatisticItemType;
stateObj?: HassEntity;
}
const MISSING_ID = "___missing-entity___";
const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[];
@customElement("ha-statistic-combo-box")
export class HaStatisticComboBox extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property({ attribute: "statistic-types" })
public statisticTypes?: "mean" | "sum";
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@property({ attribute: false, type: Array })
public statisticIds?: StatisticsMetaData[];
@property({ type: Boolean }) public disabled = false;
/**
* Show only statistics natively stored with these units of measurements.
* @type {Array}
* @attr include-statistics-unit-of-measurement
*/
@property({
type: Array,
attribute: "include-statistics-unit-of-measurement",
})
public includeStatisticsUnitOfMeasurement?: string | string[];
/**
* Show only statistics with these unit classes.
* @attr include-unit-class
*/
@property({ attribute: "include-unit-class" })
public includeUnitClass?: string | string[];
/**
* Show only statistics with these device classes.
* @attr include-device-class
*/
@property({ attribute: "include-device-class" })
public includeDeviceClass?: string | string[];
/**
* Show only statistics on entities.
* @type {Boolean}
* @attr entities-only
*/
@property({ type: Boolean, attribute: "entities-only" })
public entitiesOnly = false;
/**
* List of statistics to be excluded.
* @type {Array}
* @attr exclude-statistics
*/
@property({ type: Array, attribute: "exclude-statistics" })
public excludeStatistics?: string[];
@property({ attribute: false }) public helpMissingEntityUrl =
"/more-info/statistics/";
@state() private _opened = false;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _initialItems = false;
private _items: StatisticItem[] = [];
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.hass.loadBackendTranslation("title");
}
private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = (
item,
{ index }
) => {
const showEntityId = this.hass.userData?.showEntityIdPicker;
return html`
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
${item.icon_path
? html`
<ha-svg-icon
style="margin: 0 4px"
slot="start"
.path=${item.icon_path}
></ha-svg-icon>
`
: item.stateObj
? html`
<state-badge
slot="start"
.stateObj=${item.stateObj}
.hass=${this.hass}
></state-badge>
`
: nothing}
<span slot="headline">${item.primary} </span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.id && showEntityId
? html`<span slot="supporting-text" class="code">
${item.statistic_id}
</span>`
: nothing}
</ha-combo-box-item>
`;
};
private _getItems = memoizeOne(
(
_opened: boolean,
hass: this["hass"],
statisticIds: StatisticsMetaData[],
includeStatisticsUnitOfMeasurement?: string | string[],
includeUnitClass?: string | string[],
includeDeviceClass?: string | string[],
entitiesOnly?: boolean,
excludeStatistics?: string[],
value?: string
): StatisticItem[] => {
if (!statisticIds.length) {
return [
{
id: "",
label: "",
primary: this.hass.localize(
"ui.components.statistic-picker.no_statistics"
),
},
];
}
if (includeStatisticsUnitOfMeasurement) {
const includeUnits: (string | null)[] = ensureArray(
includeStatisticsUnitOfMeasurement
);
statisticIds = statisticIds.filter((meta) =>
includeUnits.includes(meta.statistics_unit_of_measurement)
);
}
if (includeUnitClass) {
const includeUnitClasses: (string | null)[] =
ensureArray(includeUnitClass);
statisticIds = statisticIds.filter((meta) =>
includeUnitClasses.includes(meta.unit_class)
);
}
if (includeDeviceClass) {
const includeDeviceClasses: (string | null)[] =
ensureArray(includeDeviceClass);
statisticIds = statisticIds.filter((meta) => {
const stateObj = this.hass.states[meta.statistic_id];
if (!stateObj) {
return true;
}
return includeDeviceClasses.includes(
stateObj.attributes.device_class || ""
);
});
}
const isRTL = computeRTL(this.hass);
const output: StatisticItem[] = [];
statisticIds.forEach((meta) => {
if (
excludeStatistics &&
meta.statistic_id !== value &&
excludeStatistics.includes(meta.statistic_id)
) {
return;
}
const stateObj = this.hass.states[meta.statistic_id];
if (!stateObj) {
if (!entitiesOnly) {
const id = meta.statistic_id;
const label = getStatisticLabel(this.hass, meta.statistic_id, meta);
const type =
meta.statistic_id.includes(":") &&
!meta.statistic_id.includes(".")
? "external"
: "no_state";
if (type === "no_state") {
output.push({
id,
primary: label,
secondary: this.hass.localize(
"ui.components.statistic-picker.no_state"
),
label: "",
type,
sorting_label: label,
search_labels: [label, id],
icon_path: mdiShape,
});
} else if (type === "external") {
const domain = id.split(":")[0];
const domainName = domainToName(this.hass.localize, domain);
output.push({
id,
statistic_id: id,
primary: label,
secondary: domainName,
label: "",
type,
sorting_label: label,
search_labels: [label, domainName, id],
icon_path: mdiChartLine,
});
}
}
return;
}
const id = meta.statistic_id;
const { area, device } = getEntityContext(stateObj, hass);
const friendlyName = computeStateName(stateObj); // Keep this for search
const entityName = computeEntityName(stateObj, hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const primary = entityName || deviceName || id;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
output.push({
id,
statistic_id: id,
label: "",
primary,
secondary,
stateObj: stateObj,
type: "entity",
sorting_label: [deviceName, entityName].join("_"),
search_labels: [
entityName,
deviceName,
areaName,
friendlyName,
id,
].filter(Boolean) as string[],
});
});
if (!output.length) {
return [
{
id: "",
primary: this.hass.localize(
"ui.components.statistic-picker.no_match"
),
label: "",
},
];
}
if (output.length > 1) {
output.sort((a, b) => {
const aPrefix = TYPE_ORDER.indexOf(a.type || "no_state");
const bPrefix = TYPE_ORDER.indexOf(b.type || "no_state");
return caseInsensitiveStringCompare(
`${aPrefix}_${a.sorting_label || ""}`,
`${bPrefix}_${b.sorting_label || ""}`,
this.hass.locale.language
);
});
}
output.push({
id: MISSING_ID,
primary: this.hass.localize(
"ui.components.statistic-picker.missing_entity"
),
label: "",
icon_path: mdiHelpCircle,
});
return output;
}
);
public async open() {
await this.updateComplete;
await this.comboBox?.open();
}
public async focus() {
await this.updateComplete;
await this.comboBox?.focus();
}
protected shouldUpdate(changedProps: PropertyValues) {
if (
changedProps.has("value") ||
changedProps.has("label") ||
changedProps.has("disabled")
) {
return true;
}
return !(!changedProps.has("_opened") && this._opened);
}
public willUpdate(changedProps: PropertyValues) {
if (
(!this.hasUpdated && !this.statisticIds) ||
changedProps.has("statisticTypes")
) {
this._getStatisticIds();
}
if (
this.statisticIds &&
(!this._initialItems || (changedProps.has("_opened") && this._opened))
) {
this._items = this._getItems(
this._opened,
this.hass,
this.statisticIds!,
this.includeStatisticsUnitOfMeasurement,
this.includeUnitClass,
this.includeDeviceClass,
this.entitiesOnly,
this.excludeStatistics,
this.value
);
if (this._initialItems) {
this.comboBox.filteredItems = this._items;
}
this._initialItems = true;
}
}
protected render(): TemplateResult | typeof nothing {
if (this._items.length === 0) {
return nothing;
}
return html`
<ha-combo-box
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.statistic-picker.statistic")
: this.label}
.value=${this._value}
.renderer=${this._rowRenderer}
.disabled=${this.disabled}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._items}
item-value-path="id"
item-id-path="id"
item-label-path="label"
@opened-changed=${this._openedChanged}
@value-changed=${this._statisticChanged}
@filter-changed=${this._filterChanged}
></ha-combo-box>
`;
}
private async _getStatisticIds() {
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
}
private get _value() {
return this.value || "";
}
private _statisticChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
let newValue = ev.detail.value;
if (newValue === MISSING_ID) {
newValue = "";
window.open(
documentationUrl(this.hass, this.helpMissingEntityUrl),
"_blank"
);
}
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _fuseIndex = memoizeOne((states: StatisticItem[]) =>
Fuse.createIndex(["search_labels"], states)
);
private _filterChanged(ev: CustomEvent): void {
if (!this._opened) return;
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.trim().toLowerCase() as string;
const index = this._fuseIndex(this._items);
const fuse = new HaFuse(this._items, {}, index);
const results = fuse.multiTermsSearch(filterString);
if (results) {
target.filteredItems = results.map((result) => result.item);
} else {
target.filteredItems = this._items;
}
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-statistic-combo-box": HaStatisticComboBox;
}
}
+298 -374
View File
@@ -1,65 +1,66 @@
import { mdiChartLine } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import Fuse from "fuse.js";
import { mdiChartLine, mdiClose, mdiMenuDown, mdiShape } from "@mdi/js";
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import {
css,
html,
LitElement,
nothing,
type CSSResultGroup,
type PropertyValues,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
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 { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityContext } from "../../common/entity/get_entity_context";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { computeRTL } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce";
import { domainToName } from "../../data/integration";
import type { StatisticsMetaData } from "../../data/recorder";
import { getStatisticIds, getStatisticLabel } from "../../data/recorder";
import { HaFuse } from "../../resources/fuse";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import {
getStatisticIds,
getStatisticLabel,
type StatisticsMetaData,
} from "../../data/recorder";
import type { HomeAssistant } from "../../types";
import "../ha-combo-box-item";
import "../ha-icon-button";
import type { HaMdListItem } from "../ha-md-list-item";
import "../ha-svg-icon";
import "./ha-entity-combo-box";
import type { HaEntityComboBox } from "./ha-entity-combo-box";
import "./ha-statistic-combo-box";
import "./state-badge";
type StatisticItemType = "entity" | "external" | "no_state";
interface StatisticItem {
id: string;
label: string;
primary: string;
secondary?: string;
show_entity_id?: boolean;
entity_name?: string;
area_name?: string;
device_name?: string;
friendly_name?: string;
sorting_label?: string;
state?: HassEntity;
type?: StatisticItemType;
iconPath?: string;
stateObj?: HassEntity;
}
const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[];
const ENTITY_ID_STYLE = styleMap({
fontFamily: "var(--code-font-family, monospace)",
fontSize: "11px",
});
@customElement("ha-statistic-picker")
export class HaStatisticPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property() public label?: string;
@property() public value?: string;
@property() public helper?: string;
@property() public placeholder?: string;
@property({ attribute: "statistic-types" })
public statisticTypes?: "mean" | "sum";
@@ -69,8 +70,6 @@ export class HaStatisticPicker extends LitElement {
@property({ attribute: false, type: Array })
public statisticIds?: StatisticsMetaData[];
@property({ type: Boolean }) public disabled = false;
/**
* Show only statistics natively stored with these units of measurements.
* @type {Array}
@@ -112,251 +111,15 @@ export class HaStatisticPicker extends LitElement {
@property({ type: Array, attribute: "exclude-statistics" })
public excludeStatistics?: string[];
@property({ attribute: false }) public helpMissingEntityUrl =
"/more-info/statistics/";
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@query("#anchor") private _anchor?: HaMdListItem;
@query("#input") private _input?: HaEntityComboBox;
@state() private _opened = false;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _initialItems = false;
private _items: StatisticItem[] = [];
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.hass.loadBackendTranslation("title");
}
private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = (
item,
{ index }
) => html`
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
${!item.state
? html`<ha-svg-icon
style="margin: 0 4px"
slot="start"
.path=${item.iconPath}
></ha-svg-icon>`
: html`
<state-badge
slot="start"
.stateObj=${item.state}
.hass=${this.hass}
></state-badge>
`}
<span slot="headline">${item.primary} </span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.id && item.show_entity_id
? html`
<span slot="supporting-text" style=${ENTITY_ID_STYLE}>
${item.id}
</span>
`
: nothing}
</ha-combo-box-item>
`;
private _getItems = memoizeOne(
(
_opened: boolean,
hass: this["hass"],
statisticIds: StatisticsMetaData[],
includeStatisticsUnitOfMeasurement?: string | string[],
includeUnitClass?: string | string[],
includeDeviceClass?: string | string[],
entitiesOnly?: boolean,
excludeStatistics?: string[],
value?: string
): StatisticItem[] => {
if (!statisticIds.length) {
return [
{
id: "",
label: this.hass.localize(
"ui.components.statistic-picker.no_statistics"
),
primary: this.hass.localize(
"ui.components.statistic-picker.no_statistics"
),
},
];
}
if (includeStatisticsUnitOfMeasurement) {
const includeUnits: (string | null)[] = ensureArray(
includeStatisticsUnitOfMeasurement
);
statisticIds = statisticIds.filter((meta) =>
includeUnits.includes(meta.statistics_unit_of_measurement)
);
}
if (includeUnitClass) {
const includeUnitClasses: (string | null)[] =
ensureArray(includeUnitClass);
statisticIds = statisticIds.filter((meta) =>
includeUnitClasses.includes(meta.unit_class)
);
}
if (includeDeviceClass) {
const includeDeviceClasses: (string | null)[] =
ensureArray(includeDeviceClass);
statisticIds = statisticIds.filter((meta) => {
const stateObj = this.hass.states[meta.statistic_id];
if (!stateObj) {
return true;
}
return includeDeviceClasses.includes(
stateObj.attributes.device_class || ""
);
});
}
const isRTL = computeRTL(this.hass);
const output: StatisticItem[] = [];
statisticIds.forEach((meta) => {
if (
excludeStatistics &&
meta.statistic_id !== value &&
excludeStatistics.includes(meta.statistic_id)
) {
return;
}
const entityState = this.hass.states[meta.statistic_id];
if (!entityState) {
if (!entitiesOnly) {
const id = meta.statistic_id;
const label = getStatisticLabel(this.hass, meta.statistic_id, meta);
const type =
meta.statistic_id.includes(":") &&
!meta.statistic_id.includes(".")
? "external"
: "no_state";
if (type === "no_state") {
output.push({
id,
primary: label,
secondary: this.hass.localize(
"ui.components.statistic-picker.no_state"
),
label,
type,
sorting_label: label,
});
} else if (type === "external") {
const domain = id.split(":")[0];
const domainName = domainToName(this.hass.localize, domain);
output.push({
id,
primary: label,
secondary: domainName,
label,
type,
sorting_label: label,
iconPath: mdiChartLine,
});
}
}
return;
}
const id = meta.statistic_id;
const { area, device } = getEntityContext(entityState, hass);
const friendlyName = computeStateName(entityState); // Keep this for search
const entityName = computeEntityName(entityState, hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const primary = entityName || deviceName || id;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
output.push({
id,
primary,
secondary,
label: friendlyName,
state: entityState,
type: "entity",
sorting_label: [deviceName, entityName].join("_"),
entity_name: entityName || deviceName,
area_name: areaName,
device_name: deviceName,
friendly_name: friendlyName,
show_entity_id: hass.userData?.showEntityIdPicker,
});
});
if (!output.length) {
return [
{
id: "",
primary: this.hass.localize(
"ui.components.statistic-picker.no_match"
),
label: this.hass.localize(
"ui.components.statistic-picker.no_match"
),
},
];
}
if (output.length > 1) {
output.sort((a, b) => {
const aPrefix = TYPE_ORDER.indexOf(a.type || "no_state");
const bPrefix = TYPE_ORDER.indexOf(b.type || "no_state");
return caseInsensitiveStringCompare(
`${aPrefix}_${a.sorting_label || ""}`,
`${bPrefix}_${b.sorting_label || ""}`,
this.hass.locale.language
);
});
}
output.push({
id: "__missing",
primary: this.hass.localize(
"ui.components.statistic-picker.missing_entity"
),
label: this.hass.localize(
"ui.components.statistic-picker.missing_entity"
),
});
return output;
}
);
public async open() {
await this.updateComplete;
await this.comboBox?.open();
}
public async focus() {
await this.updateComplete;
await this.comboBox?.focus();
}
protected shouldUpdate(changedProps: PropertyValues) {
if (
changedProps.has("value") ||
changedProps.has("label") ||
changedProps.has("disabled")
) {
return true;
}
return !(!changedProps.has("_opened") && this._opened);
}
public willUpdate(changedProps: PropertyValues) {
if (
(!this.hasUpdated && !this.statisticIds) ||
@@ -364,117 +127,278 @@ export class HaStatisticPicker extends LitElement {
) {
this._getStatisticIds();
}
if (
this.statisticIds &&
(!this._initialItems || (changedProps.has("_opened") && this._opened))
) {
this._items = this._getItems(
this._opened,
this.hass,
this.statisticIds!,
this.includeStatisticsUnitOfMeasurement,
this.includeUnitClass,
this.includeDeviceClass,
this.entitiesOnly,
this.excludeStatistics,
this.value
);
if (this._initialItems) {
this.comboBox.filteredItems = this._items;
}
this._initialItems = true;
}
}
protected render(): TemplateResult | typeof nothing {
if (this._items.length === 0) {
return nothing;
}
return html`
<ha-combo-box
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.statistic-picker.statistic")
: this.label}
.value=${this._value}
.renderer=${this._rowRenderer}
.disabled=${this.disabled}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._items}
item-value-path="id"
item-id-path="id"
item-label-path="label"
@opened-changed=${this._openedChanged}
@value-changed=${this._statisticChanged}
@filter-changed=${this._filterChanged}
></ha-combo-box>
`;
}
private async _getStatisticIds() {
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
}
private get _value() {
return this.value || "";
}
private _statisticChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
let newValue = ev.detail.value;
if (newValue === "__missing") {
newValue = "";
private _statisticMetaData = memoizeOne(
(statisticId: string, statisticIds: StatisticsMetaData[]) => {
if (!statisticIds) {
return undefined;
}
return statisticIds.find(
(statistic) => statistic.statistic_id === statisticId
);
}
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _fuseIndex = memoizeOne((states: StatisticItem[]) =>
Fuse.createIndex(
[
"label",
"entity_name",
"device_name",
"area_name",
"friendly_name", // for backwards compatibility
"id", // for technical search
],
states
)
);
private _filterChanged(ev: CustomEvent): void {
if (!this._opened) return;
private _renderContent() {
const statisticId = this.value || "";
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.trim().toLowerCase() as string;
if (!this.value) {
return html`
<span slot="headline" class="placeholder"
>${this.placeholder ??
this.hass.localize(
"ui.components.statistic-picker.placeholder"
)}</span
>
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
`;
}
const index = this._fuseIndex(this._items);
const fuse = new HaFuse(this._items, {}, index);
const item = this._computeItem(statisticId);
const results = fuse.multiTermsSearch(filterString);
const showClearIcon =
!this.required && !this.disabled && !this.hideClearIcon;
if (results) {
target.filteredItems = results.map((result) => result.item);
} else {
target.filteredItems = this._items;
return html`
${item.stateObj
? html`
<state-badge
.hass=${this.hass}
.stateObj=${item.stateObj}
slot="start"
></state-badge>
`
: item.iconPath
? html`<ha-svg-icon
slot="start"
.path=${item.iconPath}
></ha-svg-icon>`
: nothing}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${showClearIcon
? html`<ha-icon-button
class="clear"
slot="end"
@click=${this._clear}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
`;
}
private _computeItem(statisticId: string): StatisticItem {
const stateObj = this.hass.states[statisticId];
if (stateObj) {
const { area, device } = getEntityContext(stateObj, this.hass);
const entityName = computeEntityName(stateObj, this.hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const isRTL = computeRTL(this.hass);
const primary = entityName || deviceName || statisticId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
return {
primary,
secondary,
stateObj,
};
}
const statistic = this.statisticIds
? this._statisticMetaData(statisticId, this.statisticIds)
: undefined;
if (statistic) {
const type =
statisticId.includes(":") && !statisticId.includes(".")
? "external"
: "no_state";
if (type === "external") {
const label = getStatisticLabel(this.hass, statisticId, statistic);
const domain = statisticId.split(":")[0];
const domainName = domainToName(this.hass.localize, domain);
return {
primary: label,
secondary: domainName,
iconPath: mdiChartLine,
};
}
}
return {
primary: statisticId,
iconPath: mdiShape,
};
}
protected render() {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<div class="container">
${!this._opened
? html`
<ha-combo-box-item
.disabled=${this.disabled}
id="anchor"
type="button"
compact
@click=${this._showPicker}
>
${this._renderContent()}
</ha-combo-box-item>
`
: html`
<ha-statistic-combo-box
id="input"
.hass=${this.hass}
.autofocus=${this.autofocus}
.allowCustomEntity=${this.allowCustomEntity}
.label=${this.hass.localize("ui.common.search")}
.value=${this.value}
.includeStatisticsUnitOfMeasurement=${this
.includeStatisticsUnitOfMeasurement}
.includeUnitClass=${this.includeUnitClass}
.includeDeviceClass=${this.includeDeviceClass}
.statisticTypes=${this.statisticTypes}
.statisticIds=${this.statisticIds}
.excludeStatistics=${this.excludeStatistics}
hide-clear-icon
@opened-changed=${this._debounceOpenedChanged}
@input=${stopPropagation}
></ha-statistic-combo-box>
`}
${this._renderHelper()}
</div>
`;
}
private _renderHelper() {
return this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: nothing;
}
private _clear(e) {
e.stopPropagation();
this.value = undefined;
fireEvent(this, "value-changed", { value: undefined });
fireEvent(this, "change");
}
private async _showPicker() {
if (this.disabled) {
return;
}
this._opened = true;
await this.updateComplete;
this._input?.focus();
this._input?.open();
}
// Multiple calls to _openedChanged can be triggered in quick succession
// when the menu is opened
private _debounceOpenedChanged = debounce(
(ev) => this._openedChanged(ev),
10
);
private async _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
const opened = ev.detail.value;
if (this._opened && !opened) {
this._opened = false;
await this.updateComplete;
this._anchor?.focus();
}
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
static get styles(): CSSResultGroup {
return [
css`
.container {
position: relative;
display: block;
}
ha-combo-box-item {
background-color: var(--mdc-text-field-fill-color, whitesmoke);
border-radius: 4px;
border-end-end-radius: 0;
border-end-start-radius: 0;
--md-list-item-one-line-container-height: 56px;
--md-list-item-two-line-container-height: 56px;
--md-list-item-top-space: 8px;
--md-list-item-bottom-space: 8px;
--md-list-item-leading-space: 8px;
--md-list-item-trailing-space: 8px;
--ha-md-list-item-gap: 8px;
/* Remove the default focus ring */
--md-focus-ring-width: 0px;
--md-focus-ring-duration: 0s;
}
/* Add Similar focus style as the text field */
ha-combo-box-item:after {
display: block;
content: "";
position: absolute;
pointer-events: none;
bottom: 0;
left: 0;
right: 0;
height: 1px;
width: 100%;
background-color: var(
--mdc-text-field-idle-line-color,
rgba(0, 0, 0, 0.42)
);
transform:
height 180ms ease-in-out,
background-color 180ms ease-in-out;
}
ha-combo-box-item:focus:after {
height: 2px;
background-color: var(--mdc-theme-primary);
}
ha-combo-box-item ha-svg-icon[slot="start"] {
margin: 0 4px;
}
.clear {
margin: 0 -8px;
--mdc-icon-button-size: 32px;
--mdc-icon-size: 20px;
}
.edit {
--mdc-icon-size: 20px;
width: 32px;
}
label {
display: block;
margin: 0 0 8px;
}
.placeholder {
color: var(--secondary-text-color);
padding: 0 8px;
}
`,
];
}
}
+10 -6
View File
@@ -16,11 +16,11 @@ class HaStatisticsPicker extends LitElement {
@property({ attribute: "statistic-types" })
public statisticTypes?: "mean" | "sum";
@property({ attribute: "picked-statistic-label" })
public pickedStatisticLabel?: string;
@property({ type: String })
public label?: string;
@property({ attribute: "pick-statistic-label" })
public pickStatisticLabel?: string;
@property({ type: String })
public placeholder?: string;
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@@ -82,6 +82,7 @@ class HaStatisticsPicker extends LitElement {
: this.statisticTypes;
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
${repeat(
this._currentStatistics,
(statisticId) => statisticId,
@@ -96,7 +97,6 @@ class HaStatisticsPicker extends LitElement {
.value=${statisticId}
.statisticTypes=${includeStatisticTypesCurrent}
.statisticIds=${this.statisticIds}
.label=${this.pickedStatisticLabel}
.excludeStatistics=${this.value}
.allowCustomEntity=${this.allowCustomEntity}
@value-changed=${this._statisticChanged}
@@ -113,7 +113,7 @@ class HaStatisticsPicker extends LitElement {
.includeDeviceClass=${this.includeDeviceClass}
.statisticTypes=${this.statisticTypes}
.statisticIds=${this.statisticIds}
.label=${this.pickStatisticLabel}
.placeholder=${this.placeholder}
.excludeStatistics=${this.value}
.allowCustomEntity=${this.allowCustomEntity}
@value-changed=${this._addStatistic}
@@ -181,6 +181,10 @@ class HaStatisticsPicker extends LitElement {
width: 100%;
margin-top: 8px;
}
label {
display: block;
margin-bottom: 0 0 8px;
}
`;
}
+1 -1
View File
@@ -108,7 +108,7 @@ class StateInfo extends LitElement {
.name.in-dialog,
:host([secondary-line]) .name {
line-height: var(--ha-line-height-normal);
line-height: 20px;
}
.time-ago,
+1 -1
View File
@@ -129,7 +129,7 @@ class HaAlert extends LitElement {
}
.title {
margin-top: 2px;
font-weight: var(--ha-font-weight-bold);
font-weight: bold;
}
.action mwc-button,
.action ha-icon-button {
+1 -1
View File
@@ -56,7 +56,7 @@ export class HaAnsiToHtml extends LitElement {
overflow-wrap: break-word;
}
.bold {
font-weight: var(--ha-font-weight-bold);
font-weight: bold;
}
.italic {
font-style: italic;
+1 -1
View File
@@ -44,7 +44,7 @@ export class HaAreasDisplayEditor extends LitElement {
);
const items: DisplayItem[] = areas.map((area) => {
const { floor } = getAreaContext(area.area_id, this.hass!);
const { floor } = getAreaContext(area, this.hass!);
return {
value: area.area_id,
label: area.name,
+2 -2
View File
@@ -595,7 +595,7 @@ export class HaAssistChat extends LitElement {
}
.message {
white-space: pre-line;
font-size: var(--ha-font-size-l);
font-size: 18px;
clear: both;
margin: 8px 0;
padding: 8px;
@@ -604,7 +604,7 @@ export class HaAssistChat extends LitElement {
@media all and (max-width: 450px), all and (max-height: 500px) {
.message {
font-size: var(--ha-font-size-l);
font-size: 16px;
}
}
+7 -7
View File
@@ -95,23 +95,23 @@ export class HaBadge extends LitElement {
text-align: center;
}
.label {
font-size: var(--ha-font-size-xs);
font-size: 10px;
font-style: normal;
font-weight: var(--ha-font-weight-semibold);
line-height: var(--ha-line-height-condensed);
font-weight: 500;
line-height: 10px;
letter-spacing: 0.1px;
color: var(--secondary-text-color);
}
.content {
font-size: var(--ha-font-size-badge, var(--ha-font-size-s));
font-size: var(--ha-badge-font-size, var(--ha-font-size-s));
font-style: normal;
font-weight: var(--ha-font-weight-semibold);
line-height: var(--ha-line-height-normal);
font-weight: 500;
line-height: 16px;
letter-spacing: 0.1px;
color: var(--primary-text-color);
}
::slotted([slot="icon"]) {
--mdc-icon-size: var(--ha-icon-size-badge, 18px);
--mdc-icon-size: var(--ha-badge-icon-size, 18px);
color: var(--badge-color);
line-height: 0;
margin-left: -4px;
+4 -10
View File
@@ -387,15 +387,9 @@ export class HaBaseTimeInput extends LitElement {
--mdc-typography-body2-font-family,
var(--mdc-typography-font-family, Roboto, sans-serif)
);
font-size: var(--mdc-typography-body2-font-size, var(--ha-font-size-s));
line-height: var(
--mdc-typography-body2-line-height,
var(--ha-line-height-normal)
);
font-weight: var(
--mdc-typography-body2-font-weight,
var(--ha-font-weight-normal)
);
font-size: var(--mdc-typography-body2-font-size, 0.875rem);
line-height: var(--mdc-typography-body2-line-height, 1.25rem);
font-weight: var(--mdc-typography-body2-font-weight, 400);
letter-spacing: var(
--mdc-typography-body2-letter-spacing,
0.0178571429em
@@ -409,7 +403,7 @@ export class HaBaseTimeInput extends LitElement {
}
ha-input-helper-text {
padding-top: 8px;
line-height: var(--ha-line-height-normal);
line-height: normal;
}
`;
}
+3 -3
View File
@@ -59,7 +59,7 @@ export class HaBigNumber extends LitElement {
css`
:host {
font-size: 57px;
line-height: var(--ha-line-height-condensed);
line-height: 1.12;
letter-spacing: -0.25px;
}
.value {
@@ -87,12 +87,12 @@ export class HaBigNumber extends LitElement {
}
.value .decimal {
font-size: 0.42em;
line-height: var(--ha-line-height-condensed);
line-height: 1.33;
min-height: 1.33em;
}
.value .unit {
font-size: 0.33em;
line-height: var(--ha-line-height-condensed);
line-height: 1.26;
}
/* Accessibility */
.visually-hidden {
+3 -3
View File
@@ -41,14 +41,14 @@ export class HaCard extends LitElement {
:host ::slotted(.card-header) {
color: var(--ha-card-header-color, var(--primary-text-color));
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;
line-height: var(--ha-line-height-expanded);
line-height: 48px;
padding: 12px 16px 16px;
display: block;
margin-block-start: 0px;
margin-block-end: 0px;
font-weight: var(--ha-font-weight-normal);
font-weight: normal;
}
:host ::slotted(.card-content:not(:first-child)),
+1 -1
View File
@@ -154,7 +154,7 @@ class HaClimateState extends LitElement {
}
.state-label {
font-weight: var(--ha-font-weight-bold);
font-weight: bold;
}
.unit {
+21 -4
View File
@@ -17,14 +17,17 @@ export class HaComboBoxItem extends HaMdListItem {
:host([border-top]) md-item {
border-top: 1px solid var(--divider-color);
}
[slot="start"] {
--state-icon-color: var(--secondary-text-color);
}
[slot="headline"] {
line-height: var(--ha-line-height-normal);
font-size: var(--ha-font-size-m);
line-height: 22px;
font-size: 14px;
white-space: nowrap;
}
[slot="supporting-text"] {
line-height: var(--ha-line-height-normal);
font-size: var(--ha-font-size-s);
line-height: 18px;
font-size: 12px;
white-space: nowrap;
}
::slotted(state-badge),
@@ -32,6 +35,20 @@ export class HaComboBoxItem extends HaMdListItem {
width: 32px;
height: 32px;
}
::slotted(.code) {
font-family: var(--ha-font-family-code);
font-size: var(--ha-font-size-xs);
}
[slot="trailing-supporting-text"] {
font-size: var(--ha-font-size-s);
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-normal);
align-self: flex-end;
max-width: 30%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
`,
];
}
+4
View File
@@ -147,6 +147,10 @@ export class HaComboBox extends LitElement {
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 -->
+1 -1
View File
@@ -59,7 +59,7 @@ export class HaControlButton extends LitElement {
box-sizing: border-box;
line-height: inherit;
font-family: Roboto;
font-weight: var(--ha-font-weight-semibold);
font-weight: 500;
outline: none;
overflow: hidden;
background: none;
+1 -1
View File
@@ -194,7 +194,7 @@ export class HaControlNumberButton extends LitElement {
color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent;
font-style: normal;
font-weight: var(--ha-font-weight-semibold);
font-weight: 500;
transition: color 180ms ease-in-out;
}
:host([disabled]) {
+3 -3
View File
@@ -179,8 +179,8 @@ export class HaControlSelectMenu extends SelectBase {
--control-select-menu-padding: 6px 10px;
--mdc-icon-size: 20px;
--ha-ripple-color: var(--secondary-text-color);
font-size: var(--ha-font-size-m);
line-height: var(--ha-line-height-condensed);
font-size: 14px;
line-height: 1.4;
width: auto;
color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent;
@@ -208,7 +208,7 @@ export class HaControlSelectMenu extends SelectBase {
width: 100%;
user-select: none;
font-style: normal;
font-weight: var(--ha-font-weight-normal);
font-weight: 400;
letter-spacing: 0.25px;
}
.content {
+1 -1
View File
@@ -207,7 +207,7 @@ export class HaControlSelect extends LitElement {
outline: none;
transition: box-shadow 180ms ease-in-out;
font-style: normal;
font-weight: var(--ha-font-weight-semibold);
font-weight: 500;
color: var(--primary-text-color);
user-select: none;
-webkit-tap-highlight-color: transparent;
+1 -1
View File
@@ -368,7 +368,7 @@ export class HaControlSlider extends LitElement {
--control-slider-background-opacity: 0.2;
--control-slider-thickness: 40px;
--control-slider-border-radius: 10px;
--control-slider-tooltip-font-size: var(--ha-font-size-m);
--control-slider-tooltip-font-size: 14px;
height: var(--control-slider-thickness);
width: 100%;
border-radius: var(--control-slider-border-radius);
+5 -5
View File
@@ -53,13 +53,13 @@ export class HaDialogHeader extends LitElement {
white-space: nowrap;
}
.header-title {
font-size: var(--ha-font-size-xl);
line-height: var(--ha-line-height-normal);
font-weight: var(--ha-font-weight-normal);
font-size: 22px;
line-height: 28px;
font-weight: 400;
}
.header-subtitle {
font-size: var(--ha-font-size-m);
line-height: var(--ha-line-height-condensed);
font-size: 14px;
line-height: 20px;
color: var(--secondary-text-color);
}
@media all and (min-width: 450px) and (min-height: 500px) {
+2 -2
View File
@@ -85,8 +85,8 @@ export class HaDialog extends DialogBase {
var(--dialog-backdrop-filter, none)
);
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
--mdc-typography-headline6-font-size: var(--ha-font-size-xl);
--mdc-typography-headline6-font-weight: 400;
--mdc-typography-headline6-font-size: 1.574rem;
}
.mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end);
+2 -2
View File
@@ -188,7 +188,7 @@ export class HaExpansionPanel extends LitElement {
align-items: center;
cursor: pointer;
overflow: hidden;
font-weight: var(--ha-font-weight-semibold);
font-weight: 500;
outline: none;
}
#summary.noCollapse {
@@ -218,7 +218,7 @@ export class HaExpansionPanel extends LitElement {
.secondary {
display: block;
color: var(--secondary-text-color);
font-size: var(--ha-font-size-s);
font-size: 12px;
}
`;
}
+3 -3
View File
@@ -294,7 +294,7 @@ export class HaFileUpload extends LitElement {
}
.supports {
color: var(--secondary-text-color);
font-size: var(--ha-font-size-s);
font-size: 12px;
}
:host([disabled]) .secondary {
color: var(--disabled-text-color);
@@ -324,7 +324,7 @@ export class HaFileUpload extends LitElement {
box-sizing: border-box;
}
.header {
font-weight: var(--ha-font-weight-semibold);
font-weight: 500;
}
.progress {
color: var(--secondary-text-color);
@@ -333,7 +333,7 @@ export class HaFileUpload extends LitElement {
background: none;
border: none;
padding: 0;
font-size: var(--ha-font-size-m);
font-size: 14px;
color: var(--primary-color);
text-decoration: underline;
cursor: pointer;
+3 -3
View File
@@ -208,10 +208,10 @@ export class HaFilterBlueprints extends LitElement {
min-width: 16px;
box-sizing: border-box;
border-radius: 50%;
font-weight: var(--ha-font-weight-normal);
font-size: var(--ha-font-size-xs);
font-weight: 400;
font-size: 11px;
background-color: var(--primary-color);
line-height: var(--ha-line-height-normal);
line-height: 16px;
text-align: center;
padding: 0px 2px;
color: var(--text-primary-color);
+3 -3
View File
@@ -303,10 +303,10 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
min-width: 16px;
box-sizing: border-box;
border-radius: 50%;
font-weight: var(--ha-font-weight-normal);
font-size: var(--ha-font-size-xs);
font-weight: 400;
font-size: 11px;
background-color: var(--primary-color);
line-height: var(--ha-line-height-normal);
line-height: 16px;
text-align: center;
padding: 0px 2px;
color: var(--text-primary-color);
+3 -3
View File
@@ -232,10 +232,10 @@ export class HaFilterDevices extends LitElement {
min-width: 16px;
box-sizing: border-box;
border-radius: 50%;
font-weight: var(--ha-font-weight-normal);
font-size: var(--ha-font-size-xs);
font-weight: 400;
font-size: 11px;
background-color: var(--primary-color);
line-height: var(--ha-line-height-normal);
line-height: 16px;
text-align: center;
padding: 0px 2px;
color: var(--text-primary-color);
+3 -3
View File
@@ -189,10 +189,10 @@ export class HaFilterDomains extends LitElement {
min-width: 16px;
box-sizing: border-box;
border-radius: 50%;
font-weight: var(--ha-font-weight-normal);
font-size: var(--ha-font-size-xs);
font-weight: 400;
font-size: 11px;
background-color: var(--primary-color);
line-height: var(--ha-line-height-normal);
line-height: 16px;
text-align: center;
padding: 0px 2px;
color: var(--text-primary-color);
+3 -3
View File
@@ -246,10 +246,10 @@ export class HaFilterEntities extends LitElement {
min-width: 16px;
box-sizing: border-box;
border-radius: 50%;
font-weight: var(--ha-font-weight-normal);
font-size: var(--ha-font-size-xs);
font-weight: 400;
font-size: 11px;
background-color: var(--primary-color);
line-height: var(--ha-line-height-normal);
line-height: 16px;
text-align: center;
padding: 0px 2px;
color: var(--text-primary-color);
+3 -3
View File
@@ -303,10 +303,10 @@ export class HaFilterFloorAreas extends LitElement {
min-width: 16px;
box-sizing: border-box;
border-radius: 50%;
font-weight: var(--ha-font-weight-normal);
font-size: var(--ha-font-size-xs);
font-weight: 400;
font-size: 11px;
background-color: var(--primary-color);
line-height: var(--ha-line-height-normal);
line-height: 16px;
text-align: center;
padding: 0px 2px;
color: var(--text-primary-color);
+3 -3
View File
@@ -195,10 +195,10 @@ export class HaFilterIntegrations extends LitElement {
min-width: 16px;
box-sizing: border-box;
border-radius: 50%;
font-weight: var(--ha-font-weight-normal);
font-size: var(--ha-font-size-xs);
font-weight: 400;
font-size: 11px;
background-color: var(--primary-color);
line-height: var(--ha-line-height-normal);
line-height: 16px;
text-align: center;
padding: 0px 2px;
color: var(--text-primary-color);
+3 -3
View File
@@ -233,10 +233,10 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
min-width: 16px;
box-sizing: border-box;
border-radius: 50%;
font-weight: var(--ha-font-weight-normal);
font-size: var(--ha-font-size-xs);
font-weight: 400;
font-size: 11px;
background-color: var(--primary-color);
line-height: var(--ha-line-height-normal);
line-height: 16px;
text-align: center;
padding: 0px 2px;
color: var(--text-primary-color);
+3 -3
View File
@@ -177,10 +177,10 @@ export class HaFilterStates extends LitElement {
min-width: 16px;
box-sizing: border-box;
border-radius: 50%;
font-weight: var(--ha-font-weight-normal);
font-size: var(--ha-font-size-xs);
font-weight: 400;
font-size: 11px;
background-color: var(--primary-color);
line-height: var(--ha-line-height-normal);
line-height: 16px;
text-align: center;
padding: 0px 2px;
color: var(--text-primary-color);
+2 -5
View File
@@ -70,11 +70,8 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
padding-top: 4px;
box-sizing: border-box;
color: var(--secondary-text-color);
font-size: var(--ha-font-size-s);
font-weight: var(
--mdc-typography-body2-font-weight,
var(--ha-font-weight-normal)
);
font-size: 0.875rem;
font-weight: var(--mdc-typography-body2-font-weight, 400);
}
`;
}

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