Compare commits

..

374 Commits

Author SHA1 Message Date
Wendelin
f3f0dfc62b Update easing animation function 2025-02-14 11:00:08 +01:00
Wendelin
d4125d433f Add switch theme animation 2025-02-13 16:00:35 +01:00
Wendelin
c824e58e0a Show users theme even if browser theme is active 2025-02-13 14:46:38 +01:00
Wendelin
c6617718b7 Fix tests 2025-02-13 11:43:02 +01:00
Wendelin
cd1ca72e45 Fix storeState overrides user theme 2025-02-13 09:51:39 +01:00
Wendelin
4f9ca3b173 Add save theme for user functionality 2025-02-12 15:30:15 +01:00
ildar170975
1349c8520c developer-tools-template: allow "select all" for "rendered" (2) (#24171)
removed a harmful user-select
2025-02-11 13:26:46 +01:00
renovate[bot]
6d1a55cc3a Update vaadinWebComponents monorepo to v24.6.4 (#24153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 09:48:10 +01:00
Wendelin
23a9ae6835 Onboarding restore use error code (#24172)
Use error code for incorrect password
2025-02-11 09:47:18 +01:00
Wendelin
dbd1e928de Make restore button destructive (#24173) 2025-02-11 09:37:56 +01:00
renovate[bot]
e86ad21ce2 Update dependency eslint to v9.20.0 (#24169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 09:12:21 +01:00
Bram Kragten
0d97afb3f2 Add base support for sub entries (#23160)
* Add base support for sub entries

* add demo types

* fix translations

* Use sub entry name when deleting

* Update show-dialog-sub-config-flow.ts

* adjust for multiple sub types

* WIP, not functional

* add subentry_type

* rename to supported_subentry_types

* config_subentries -> config_entries_subentries

* Add localized sub flow title

* use Record

* rename

* more rename
2025-02-10 21:24:05 +01:00
Wendelin
03a415beff Onboarding restore use core api (#23920)
* Fix type issues

* Extract backup-upload

* Add onboarding upload section

* Extract and use ha-backup-details

* Implement backup details and restore

* remove unused hassio onboarding calls

* Require hass in dialog-hassio-backup

* Add restore view

* Add formatDateTime without locale and config

* Add restore status

* Fix prettier

* Fix styles of backup details

* Remove unused localize

* Fix onboarding restore translations

* Hide data-picker on core only instance

* Split uploadBackup into 2 separate funcs

* Add formatDateTimeWithBrowserDefaults

* Fix selected data for core only

* Show error reasons on status page

* Use new backup info agents

* Add mem function for formatDateTimeWithBrowserDefaults

* Fix overflow on mobile

* Handle errors when in hassio mode

* Fix cancel restore texts

* Fix hassio localize type issue

* Remove unused onboarding localize in hassio backup restore

* improve format_date_time

* Fix tests

* Fix and simplify backup upload issues

* Use foreach instead of reduce

* Fix attributes, unused styles and properties

* Simplify supervisor warning

* Fix language type issues

* Fix ha-backup-data-picker

* Improve backup-details-restore

* Fix onboarding-restore issues

* Improve loadBackupInfo

* Revert uploadBackup changes

* Improve cancel restore

* Use destructive

* Update src/panels/config/backup/dialogs/dialog-upload-backup.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Show backup type not at onboarding

* Only show backup type in correct translationPanel

* Fix quotes

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-02-10 16:40:08 +01:00
Paulus Schoutsen
44cc75afbc Require opt-in config flow navigateToResult (#24120) 2025-02-10 15:58:10 +01:00
Petar Petrov
748642a8d6 Limit max label width of hui-energy-devices-graph-card (#24152) 2025-02-10 15:54:20 +01:00
Petar Petrov
3d5c65d652 Bring back energy usage graph order (#24156) 2025-02-10 15:53:36 +01:00
Paul Bottein
a26bf80b13 Fix section border radius (#24159) 2025-02-10 15:53:05 +01:00
Petar Petrov
497c6c35f1 Fix device energy card with max_devices (#24150) 2025-02-10 10:12:42 +01:00
Petar Petrov
b0b06a2787 Round log scale limits (#24151) 2025-02-10 10:12:12 +01:00
Paulus Schoutsen
f3d55447ca Add support for intent-progress assist events (#24143) 2025-02-09 23:36:56 -05:00
renovate[bot]
1b3d4b77d3 Update dependency ua-parser-js to v2.0.1 (#24125)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-08 21:14:11 +02:00
Paulus Schoutsen
6ec4041c4c Navigate to newly created config entry (#24109) 2025-02-07 08:32:21 +02:00
karwosts
d919e8d333 Integrate Statistic Card with Energy Date Picker (#23794)
* Support energy-date-selection for statistic card

* reuse period key
2025-02-07 08:29:28 +02:00
karwosts
af7bb85667 Stack solar forecasts (#24113) 2025-02-07 06:44:39 +01:00
renovate[bot]
9061e2039b Update dependency @vitest/coverage-v8 to v3.0.5 (#24106)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-06 19:40:41 +01:00
renovate[bot]
906e6f4a88 Update dependency @codemirror/state to v6.5.2 (#24105)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-06 19:26:48 +01:00
renovate[bot]
73fbe9a69d Update typescript-eslint monorepo to v8.23.0 (#24108)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-06 19:26:16 +01:00
Petar Petrov
2a0f69a629 Fix energy dashboard data formatting (#24101) 2025-02-06 12:38:22 +01:00
Petar Petrov
9411a77f14 Set charts font to Roboto (#24097) 2025-02-06 13:16:35 +02:00
Petar Petrov
de3bf2e088 Show energy-self-sufficiency-gauge card without grid return (#24098)
Show energy-self-sufficiency-gauge card if solar is defined
2025-02-06 10:07:06 +00:00
Brynley McDonald
16181b48ae Add Mastodon and Bluesky to help tip (#24099)
Add Mastodon and Bluesky to socials tip
2025-02-06 10:59:58 +01:00
Curt Grimes
8682debe61 Fix punctuation in some toast and warning messages (#24093)
There are some toast messages that are comma splices (two independent
clauses joined by a comma), which is not correct grammar. This commit
fixes the punctuation in these messages.
2025-02-06 09:37:54 +01:00
Petar Petrov
bdbc9bc1b4 Reduce padding in energy charts and align unit (#24095) 2025-02-06 09:36:58 +01:00
renovate[bot]
79b9f8d083 Update dependency barcode-detector to v3 (#24015)
* Update dependency barcode-detector to v3

* fix breaking changes

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-02-06 07:53:26 +00:00
Wendelin
3918194d2d Add network adapter translations (#24096) 2025-02-06 09:49:05 +02:00
Charles Garwood
e9fef1f873 Update more-info dialog layout for weather entities (#22818)
* Update weather more-info style

* Cleanup unused var

* Use badges for attributes

* Remove unnecessary flex class

* CSS cleanup

* Wrap badges

* Revert "Cleanup unused var"

This reverts commit 89ab0f6ad05e1e669b84e69f4c263e3d302794f2.

* Revert badges for attributes

* Scroll long forecasts

* Use nothing instead of empty strings

* Cleanup
2025-02-06 07:11:30 +01:00
Jan-Philipp Benecke
35face602b Fix area registry dialog field (#24090) 2025-02-05 18:33:00 +01:00
Bram Kragten
803ac496f6 Merge branch 'rc' into dev 2025-02-05 16:32:25 +01:00
Bram Kragten
f1173dd84b Bumped version to 20250205.0 2025-02-05 16:27:17 +01:00
Petar Petrov
44dcca9923 Fix chart height (#24028) 2025-02-05 16:25:44 +01:00
Bram Kragten
bd74d39dd8 Use max of width and actualBoundingBox to get text width (#24085) 2025-02-05 16:20:22 +01:00
Petar Petrov
172d6c3079 Disable chart update animation (#24084) 2025-02-05 16:20:21 +01:00
Bram Kragten
56539e8065 Charts: set tooltip triggerOn to click on mobile (#24083)
set tooltip triggerOn to click on mobile
2025-02-05 16:20:20 +01:00
Bram Kragten
8f6867f142 Chart: Add tooltip styling to theme (#24082) 2025-02-05 16:20:19 +01:00
Bram Kragten
d51f8995dd Charts: add styles for legend page controls (#24081) 2025-02-05 16:20:19 +01:00
Petar Petrov
f2e35dc70a Fix chart preview (#24080)
* Fix chart preview

* Revert change to timeline-chart labels
2025-02-05 16:20:18 +01:00
Petar Petrov
6487b9b7ea Fix device energy bar chart (#24079) 2025-02-05 16:20:17 +01:00
Bram Kragten
e50b658db7 Set min height for graphs, adjust margins (#24078)
* Set min height for graphs, adjust margins

* stats + header adjustments

* set min to 200
2025-02-05 16:19:29 +01:00
Bram Kragten
6efe237639 Fix label truncated timeline chart (#24077) 2025-02-05 16:15:00 +01:00
Bram Kragten
4a94cfc05b Set list color of update more info to dialog background (#24076) 2025-02-05 16:15:00 +01:00
Bram Kragten
7cbdb1dcfd Fix condition in tracing graph (#24075) 2025-02-05 16:14:59 +01:00
Paul Bottein
553bb61db7 Fix statistic chart tooltip values (#24074) 2025-02-05 16:14:58 +01:00
Petar Petrov
786ff787d1 Fix spacing & colors in statistics-graph chart (#24068)
* Fix statistic chart colors

* Fix spacing in statistics-graph

* set start time based on data
2025-02-05 16:14:57 +01:00
Bram Kragten
28b3f2970a Use max of width and actualBoundingBox to get text width (#24085) 2025-02-05 17:12:48 +02:00
Petar Petrov
7d170a710e Disable chart update animation (#24084) 2025-02-05 15:04:03 +00:00
Bram Kragten
cc40b50675 Charts: set tooltip triggerOn to click on mobile (#24083)
set tooltip triggerOn to click on mobile
2025-02-05 15:02:43 +00:00
Bram Kragten
b6eaff46e9 Chart: Add tooltip styling to theme (#24082) 2025-02-05 16:37:13 +02:00
Bram Kragten
674bb0d16a Set min height for graphs, adjust margins (#24078)
* Set min height for graphs, adjust margins

* stats + header adjustments

* set min to 200
2025-02-05 15:16:33 +01:00
Petar Petrov
6ff018afc9 Fix chart preview (#24080)
* Fix chart preview

* Revert change to timeline-chart labels
2025-02-05 15:16:16 +01:00
Bram Kragten
ad48732bb7 Charts: add styles for legend page controls (#24081) 2025-02-05 16:04:14 +02:00
Bram Kragten
fef162346a Set list color of update more info to dialog background (#24076) 2025-02-05 12:48:13 +01:00
Bram Kragten
72d208d1ac Fix label truncated timeline chart (#24077) 2025-02-05 12:47:39 +01:00
Petar Petrov
5a8b1b0fd4 Fix device energy bar chart (#24079) 2025-02-05 12:47:21 +01:00
Bram Kragten
4cfc651799 Fix condition in tracing graph (#24075) 2025-02-05 10:04:39 +00:00
Petar Petrov
b4a3f4cb2c Fix spacing & colors in statistics-graph chart (#24068)
* Fix statistic chart colors

* Fix spacing in statistics-graph

* set start time based on data
2025-02-05 11:22:31 +02:00
Paul Bottein
f0507a88a6 Fix statistic chart tooltip values (#24074) 2025-02-05 11:18:29 +02:00
renovate[bot]
fe041e442d Update dependency vitest to v3.0.5 [SECURITY] (#24066)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-04 19:28:01 +01:00
Bram Kragten
e5fea98460 Bumped version to 20250204.0 2025-02-04 18:22:43 +01:00
Petar Petrov
31180e3a9e Fix energy charts with leap years (#24059)
* Fix energy charts with leap years

* handle quarters
2025-02-04 18:21:27 +01:00
Paul Bottein
ce0f02a45b Display unavailable backups locations (#24058)
Display anavailable backups locations
2025-02-04 18:21:27 +01:00
Paul Bottein
53f090356e Improve value formatting inside backup tooltip (#24057) 2025-02-04 18:21:26 +01:00
Bram Kragten
776c4da688 Add support package download to cloud (#24051) 2025-02-04 18:21:25 +01:00
Bram Kragten
849922f7be Dont show voice wizard for voip (#24050)
dont show voice wizard for voip
2025-02-04 18:21:24 +01:00
Paul Bottein
a26701808f Add support for add-on update type for backups in the UI (#24044)
* Add support for add-on update type for backups in the UI

* Add type to backup detail page

* Use new model

* Fix detail page

* Fix type
2025-02-04 18:21:23 +01:00
Bram Kragten
904ee2e418 Add support package download to cloud (#24051) 2025-02-04 18:06:41 +01:00
Paul Bottein
11ae3a77e8 Add support for add-on update type for backups in the UI (#24044)
* Add support for add-on update type for backups in the UI

* Add type to backup detail page

* Use new model

* Fix detail page

* Fix type
2025-02-04 16:04:11 +01:00
Paul Bottein
3a12019b64 Display unavailable backups locations (#24058)
Display anavailable backups locations
2025-02-04 14:45:38 +01:00
Bram Kragten
6c2cf1ff60 Dont show voice wizard for voip (#24050)
dont show voice wizard for voip
2025-02-04 14:38:03 +02:00
Petar Petrov
02ae0b5864 Fix energy charts with leap years (#24059)
* Fix energy charts with leap years

* handle quarters
2025-02-04 12:28:31 +00:00
Jan-Philipp Benecke
85fe2213c1 Align view mount dialog with design guidelines (#24060)
Align view mount dialog with design requirements
2025-02-04 13:25:59 +01:00
Paul Bottein
7dbc78f1d6 Improve value formatting inside backup tooltip (#24057) 2025-02-04 13:47:24 +02:00
Ingolf Becker
f965a3504f Improve weather forecast card layout (#23704)
* Improve weather forecast card layout

* fix units and improve naming

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

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-02-04 06:32:30 +00:00
karwosts
077f5efe7e Fix menus in Todo list for Keyboard (#24054) 2025-02-04 08:30:45 +02:00
Bram Kragten
ef3bea71a0 Bumped version to 20250203.0 2025-02-03 18:20:20 +01:00
Petar Petrov
fcf655b0ec FIx console errors in charts (#24048)
* FIx console errors in charts

* handle undefined unit_of_measurement
2025-02-03 18:20:17 +01:00
Paul Bottein
b263b74916 Increase margin to avoid fab overlap on backup overview page (#24047) 2025-02-03 18:20:16 +01:00
Paul Bottein
0f4b6b423a Improve chart height and narrow option in grid section (#24046)
* Fix chart size in grid

* Set minimal height to 2 for history chart

* Update history chart
2025-02-03 18:20:15 +01:00
Paul Bottein
72df585c5e Fix download unencrypted backup logic (#24045) 2025-02-03 18:20:15 +01:00
Petar Petrov
4698a63642 Show seconds on x axis when chart is zoomed a lot (#24043)
Show seconds on x axis when charts is zoomed a lot
2025-02-03 18:20:14 +01:00
Petar Petrov
6eb43a7d61 Workaround for chart size bug in editor preview (#24040) 2025-02-03 18:20:13 +01:00
Petar Petrov
af35b15400 Fix click action for timeline chart labels (#24039)
* Fix click action for timeline chart labels

* Use id in line charts
2025-02-03 18:20:12 +01:00
Petar Petrov
0d50d2664f Fix legend in charts (#24025)
* Fix legend in line charts

* fix statistics graph legend
2025-02-03 18:20:11 +01:00
Philipp
ff1159402e Fix browser media player showing more info dialog (#24021) 2025-02-03 18:20:11 +01:00
Jan-Philipp Benecke
f8742ae690 Display year conditionally when script was last triggered on script list (#24012)
Display year conditionally when script was last triggered in script list
2025-02-03 18:20:10 +01:00
ildar170975
c786d26542 Fix for "Increase generic entity row touch target (3): climate entities (#24002)
return to max-height + set vertical alignment
2025-02-03 18:20:09 +01:00
Bram Kragten
3f8ff94002 Make date period picker respect timezone settings (#23996) 2025-02-03 18:20:08 +01:00
Petar Petrov
64a968543b FIx console errors in charts (#24048)
* FIx console errors in charts

* handle undefined unit_of_measurement
2025-02-03 18:13:35 +01:00
karwosts
aea98f702b Prioritize local image over entity_picture in picture-entity card (#24032)
* Prioritize local image over entity_picture in picture-entity

* Remove the default stub image if we switch to an entity with a picture

* minor cleanup
2025-02-03 16:01:16 +01:00
Paul Bottein
863ff622be Increase margin to avoid fab overlap on backup overview page (#24047) 2025-02-03 16:00:08 +01:00
Petar Petrov
730cea6646 Workaround for chart size bug in editor preview (#24040) 2025-02-03 15:59:43 +01:00
karwosts
7d1f8d618a Fix more keyboard menus (devices/helpers/scenes/scripts) (#24037)
* Fix more keyboard menus (devices/helpers/scenes/scripts)

* Apply suggestions from code review

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

* less async

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-02-03 16:13:20 +02:00
Paul Bottein
67b970fcaa Improve chart height and narrow option in grid section (#24046)
* Fix chart size in grid

* Set minimal height to 2 for history chart

* Update history chart
2025-02-03 14:12:28 +00:00
Paul Bottein
38bcdaa6f6 Revert "Fix chart size in grid"
This reverts commit 8f1389de66.
2025-02-03 14:55:17 +01:00
Paul Bottein
8f1389de66 Fix chart size in grid 2025-02-03 14:51:19 +01:00
Paul Bottein
37ac796c8f Fix download unencrypted backup logic (#24045) 2025-02-03 14:40:49 +01:00
Petar Petrov
716cd19d41 Show seconds on x axis when chart is zoomed a lot (#24043)
Show seconds on x axis when charts is zoomed a lot
2025-02-03 13:27:33 +01:00
Paul Bottein
173725f011 Use ignoreDiacritics in fuse library (#24041) 2025-02-03 12:53:34 +01:00
Petar Petrov
ad561b885b Fix click action for timeline chart labels (#24039)
* Fix click action for timeline chart labels

* Use id in line charts
2025-02-03 10:36:59 +00:00
Philipp
d77bdf4ac6 Fix browser media player showing more info dialog (#24021) 2025-02-03 11:36:19 +01:00
Petar Petrov
ac3796ec31 Fix chart height (#24028) 2025-02-03 11:21:30 +01:00
Petar Petrov
8c3fdfb6fb Fix legend in charts (#24025)
* Fix legend in line charts

* fix statistics graph legend
2025-02-03 11:09:59 +01:00
karwosts
b7c7d0b4b5 Scroll todo list if it overflows grid_layout (#24000) 2025-02-03 09:09:01 +02:00
ildar170975
8b0e6eed3a Fix for "Increase generic entity row touch target (3): climate entities (#24002)
return to max-height + set vertical alignment
2025-02-03 09:07:11 +02:00
renovate[bot]
603f884e8c Update dependency @types/chromecast-caf-receiver to v6.0.21 (#24038)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 08:09:55 +02:00
Bram Kragten
97dfccf4c7 Make date period picker respect timezone settings (#23996) 2025-02-01 17:32:07 +01:00
Jan-Philipp Benecke
fd1e31c0cc Display year conditionally when script was last triggered on script list (#24012)
Display year conditionally when script was last triggered in script list
2025-02-01 17:16:43 +01:00
Bram Kragten
1de740e7b5 Bumped version to 20250131.0 2025-01-31 18:20:12 +01:00
Bram Kragten
5abfb90b16 fix time input width (#23998) 2025-01-31 18:19:45 +01:00
Petar Petrov
6b691063a8 Hide "heating" data from climate charts (#23997) 2025-01-31 18:19:44 +01:00
Paul Bottein
d1d746e7e6 Remove name from the chart series when using showNames = false (#23995)
* Remove name from the chart series when using showNames = false

* Remove translations
2025-01-31 18:19:43 +01:00
Petar Petrov
2fcb64d4a1 Echarts: auto scale Y in log charts (#23994)
* Echarts: auto scale Y in log charts

* fix statistics chart log scale
2025-01-31 18:19:42 +01:00
Petar Petrov
3769f8c7c0 Hide irrelevant errors from echarts zoom (#23992) 2025-01-31 18:19:41 +01:00
Paul Bottein
f0a56e75f5 Improve encrypted backup dialog (#23991)
* Improve encrypted backup dialog

* Remove unused code
2025-01-31 18:19:40 +01:00
Petar Petrov
15f33e1f19 Echarts: show all series in tooltip (#23989)
* Echarts: show all series in tooltip

* fix typo

* remove duplicate tooltip entries in statistics chart

* take last valid point instead of first
2025-01-31 18:19:40 +01:00
Petar Petrov
181122177b Echarts: fix Y scaling (#23988)
* Echarts: fix scaling of Y axis

* fix fit logic to only extend the limits

* handle invalid min for log scale
2025-01-31 18:19:39 +01:00
Petar Petrov
684cd0f627 Fix legend resetting on zoom (#23985) 2025-01-31 18:19:38 +01:00
Paul Bottein
277202e363 Use smooth line for statistic line chart (#23984)
* Use smooth line for statistic line chart

* Use same smooth options as chartjs
2025-01-31 18:19:37 +01:00
Petar Petrov
b388d1fd42 Fix statistics echarts with negative values (#23983)
* Fix statistics echarts with negative values

* fix border-radius of negative bar values

* revert timeline label width to previous max values
2025-01-31 18:19:37 +01:00
Paul Bottein
251e6399f5 Reduce chart height to 300px (#23979) 2025-01-31 18:19:36 +01:00
karwosts
f44c5d7a63 Improve statistics graph axis when using energy_date_selection (#23974) 2025-01-31 18:19:35 +01:00
ildar170975
cae1ca52f0 Fix for "Increase generic entity row touch target (2) (#23973)
* Revert "Fix for "Increase generic entity row touch target" (#23953)"

This reverts commit 028472fc7b.

* conditional style
2025-01-31 18:19:34 +01:00
Petar Petrov
f8de2c64a5 Hide "heating" data from climate charts (#23997) 2025-01-31 17:13:13 +00:00
Bram Kragten
34ef5be720 fix time input width (#23998) 2025-01-31 17:12:49 +00:00
Petar Petrov
1402802031 Echarts: auto scale Y in log charts (#23994)
* Echarts: auto scale Y in log charts

* fix statistics chart log scale
2025-01-31 17:42:51 +01:00
Paul Bottein
816989ab4d Remove name from the chart series when using showNames = false (#23995)
* Remove name from the chart series when using showNames = false

* Remove translations
2025-01-31 17:38:50 +01:00
Paul Bottein
d4497ca39c Improve encrypted backup dialog (#23991)
* Improve encrypted backup dialog

* Remove unused code
2025-01-31 16:10:43 +00:00
Petar Petrov
6e39242ca3 Echarts: fix Y scaling (#23988)
* Echarts: fix scaling of Y axis

* fix fit logic to only extend the limits

* handle invalid min for log scale
2025-01-31 15:44:22 +01:00
Petar Petrov
0197e32783 Echarts: show all series in tooltip (#23989)
* Echarts: show all series in tooltip

* fix typo

* remove duplicate tooltip entries in statistics chart

* take last valid point instead of first
2025-01-31 15:42:46 +01:00
Petar Petrov
87dfed4beb Hide irrelevant errors from echarts zoom (#23992) 2025-01-31 15:37:47 +01:00
Wendelin
dae991dc89 Improve develop and serve (#23990) 2025-01-31 15:50:15 +02:00
Paul Bottein
6197e3483b Use smooth line for statistic line chart (#23984)
* Use smooth line for statistic line chart

* Use same smooth options as chartjs
2025-01-31 11:46:01 +01:00
Petar Petrov
b2a6c8bd36 Fix legend resetting on zoom (#23985) 2025-01-31 11:13:46 +01:00
Petar Petrov
938855e13c Fix statistics echarts with negative values (#23983)
* Fix statistics echarts with negative values

* fix border-radius of negative bar values

* revert timeline label width to previous max values
2025-01-31 12:02:39 +02:00
renovate[bot]
a8712e3b8e Update vaadinWebComponents monorepo to v24.6.3 (#23981)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 10:31:11 +01:00
Paul Bottein
b15b577057 Reduce chart height to 300px (#23979) 2025-01-31 10:42:51 +02:00
karwosts
653aeae3d8 Improve statistics graph axis when using energy_date_selection (#23974) 2025-01-31 08:45:37 +02:00
ildar170975
0aea6141ad Fix for "Increase generic entity row touch target (2) (#23973)
* Revert "Fix for "Increase generic entity row touch target" (#23953)"

This reverts commit 028472fc7b.

* conditional style
2025-01-31 08:41:38 +02:00
renovate[bot]
5243c1d871 Update typescript-eslint monorepo to v8.22.0 (#23972)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 08:39:18 +02:00
Bram Kragten
9449f5ad0a Bumped version to 20250130.0 2025-01-30 18:08:13 +01:00
Paul Bottein
c337bc5f97 Improve backup settings display on mobile (#23967) 2025-01-30 18:07:52 +01:00
Petar Petrov
6aab60cf45 Dynamically reorder energy devices (echarts) (#23966)
* Dynamically reorder energy devices (echarts)

* fix initial sorting in hui-energy-devices-detail-graph-card

* fix dynamic reordering in devices detail
2025-01-30 18:07:51 +01:00
Paul Bottein
52e9bc3213 Fix backup location config not updated (#23965) 2025-01-30 18:07:50 +01:00
Paul Bottein
e48b2383cf Fix location icon when many locations in backup datatable (#23964)
* Fix location icon when many locations in backup datatable

* Reuse data

* Don't copy twice

* Improve naming
2025-01-30 18:07:50 +01:00
Petar Petrov
002a249777 Use CSS variables to theme echarts (#23963)
* Use CSS variables to theme echarts

* fix styles
2025-01-30 18:07:49 +01:00
Paul Bottein
10498ce18d Display device name in bluetooth panel (#23960) 2025-01-30 18:07:48 +01:00
Wendelin
6a5936b2b2 Add correct link to backup.create_automatic (#23959) 2025-01-30 18:07:47 +01:00
Norbert Rittel
dc68aaa803 Add localizable "Actions" label to OAuth credentials picker (#23958)
* Add localizable "Actions" label to OAuth credentials picker

* Prettier
2025-01-30 18:07:46 +01:00
Paul Bottein
e7931ce049 Restore scroll position go back to backup settings page (#23955) 2025-01-30 18:07:46 +01:00
ildar170975
59b2582fe3 Fix for "Increase generic entity row touch target" (#23953)
fix for "touch target"
2025-01-30 18:07:45 +01:00
karwosts
8577b0721c Fix untracked energy in compare (#23949) 2025-01-30 18:07:44 +01:00
J. Nick Koston
91319be855 Reduce size of address column on Bluetooth Advertisement monitor (#23942) 2025-01-30 18:07:43 +01:00
Simon Lamon
0dff538298 Backup location translations improvements (#23940)
* Backup location translations improvements

* Apply better translations
2025-01-30 18:07:42 +01:00
Simon Lamon
6ac6d9c6eb Backup location translations improvements (#23940)
* Backup location translations improvements

* Apply better translations
2025-01-30 18:06:57 +01:00
Norbert Rittel
6ba0071296 Add localizable "Actions" label to OAuth credentials picker (#23958)
* Add localizable "Actions" label to OAuth credentials picker

* Prettier
2025-01-30 18:05:18 +01:00
Paul Bottein
fef5dc4232 Fix location icon when many locations in backup datatable (#23964)
* Fix location icon when many locations in backup datatable

* Reuse data

* Don't copy twice

* Improve naming
2025-01-30 17:02:56 +00:00
Paul Bottein
ce58962dbb Fix backup location config not updated (#23965) 2025-01-30 17:43:39 +01:00
Petar Petrov
9fb1e1d2ed Dynamically reorder energy devices (echarts) (#23966)
* Dynamically reorder energy devices (echarts)

* fix initial sorting in hui-energy-devices-detail-graph-card

* fix dynamic reordering in devices detail
2025-01-30 17:43:06 +01:00
Paul Bottein
a29544c1e6 Improve backup settings display on mobile (#23967) 2025-01-30 17:49:05 +02:00
Petar Petrov
b2b71edd04 Use CSS variables to theme echarts (#23963)
* Use CSS variables to theme echarts

* fix styles
2025-01-30 14:39:59 +01:00
ildar170975
028472fc7b Fix for "Increase generic entity row touch target" (#23953)
fix for "touch target"
2025-01-30 13:26:11 +01:00
Paul Bottein
b056ce228b Display device name in bluetooth panel (#23960) 2025-01-30 12:36:10 +01:00
Wendelin
0cd4256c0e Add correct link to backup.create_automatic (#23959) 2025-01-30 11:05:33 +00:00
Yosi Levy
e274c5b23f Add node memory to allow commit (#23954) 2025-01-30 11:06:50 +02:00
karwosts
ea57846465 Fix untracked energy in compare (#23949) 2025-01-30 09:57:54 +01:00
Paul Bottein
3f2e2bc659 Restore scroll position go back to backup settings page (#23955) 2025-01-30 09:56:52 +01:00
J. Nick Koston
e3f2f66206 Reduce size of address column on Bluetooth Advertisement monitor (#23942) 2025-01-29 19:01:47 +00:00
Bram Kragten
b95e87845f Merge branch 'rc' into dev 2025-01-29 16:46:26 +01:00
Bram Kragten
bc49ebc489 Bumped version to 20250129.0 2025-01-29 16:38:25 +01:00
Wendelin
c97d0ce68a Add translations for backup agents encryption (#23938)
* Add translations for backup agents encryption

* Update src/translations/en.json

* Split class map

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2025-01-29 15:31:55 +00:00
renovate[bot]
a0e7d6f1c6 Update dependency lint-staged to v15.4.3 (#23937)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-29 16:23:04 +01:00
Petar Petrov
25d2a5ddac Open more-info on label click in echarts (#23921)
* Open more-info on label click in echarts

* check isExternalStatistic in energy devices click
2025-01-29 15:44:21 +01:00
Paul Bottein
a2f2d64f5c Allow to change encryption for each backup location (#23861)
* Add backup location settings page

* Add encryption settings

* Display unencrypted locations and backups

* Improve cloud detail page

* Fix encryption flag

* Fix restore backup

* Fix lint

* Fix translations

* Add warning

* Use agents in backup locations

* Feedback

* Use updated

* Improve encrypted/unencrypted status

* Improve code quality

* Remove hardcoded failed id

* Extend agent interface

* Use willupdate
2025-01-29 14:23:47 +01:00
Petar Petrov
fcdcbbda05 Fix statistics chart stacking and colors (#23922) 2025-01-29 12:46:59 +01:00
Paul Bottein
4b5c7fc2de Only restore config and database for core install (#23935) 2025-01-29 12:46:30 +01:00
Paul Bottein
dd64d88afe Use handle icon for drag and drop in energy devices settings (#23933)
* Use handle icon for drag and drop in energy devices settings

* Remove import
2025-01-29 11:04:09 +00:00
Wendelin
5feffd08ff Fix backup time supporting-text (#23931) 2025-01-29 09:08:26 +01:00
karwosts
543c7df3e0 Keyboard accessible menus in hass-tabs-subpage-data-table (#23927) 2025-01-29 08:23:27 +02:00
ildar170975
167f859f2a fix padding for error-log-card (#23923)
make padding similar to system-log-card
2025-01-29 08:22:24 +02:00
J. Nick Koston
c7d9699a24 Include addresses of Bluetooth devices in the connection slots tooltip (#23928) 2025-01-29 08:21:34 +02:00
ildar170975
a638cf443d fix overflow for ha-map (#23929) 2025-01-29 08:19:00 +02:00
Patrick Kortendick
438d1c13ef Fix misspelling for humidifier card description (#23924) 2025-01-29 05:20:01 +00:00
J. Nick Koston
cc8869b9f9 Show scanner name in the Bluetooth Advertisement Monitor (#23926) 2025-01-28 13:07:16 -10:00
J. Nick Koston
cbdb7406ad Add support for showing Bluetooth connection slot allocations (#23899) 2025-01-28 06:52:21 -10:00
Wendelin
f8d2560104 Revert "Add shortcut hint to assist dialog" (#23918)
Revert "Add shortcut hint to assist dialog (#23739)"

This reverts commit d121b33263.
2025-01-28 10:53:57 -05:00
Petar Petrov
cc48ae82d6 Fixes for echarts (#23906)
* show negative untracked energy again

* fix chart cards height

* fix timeline label width

* fix statistics chart legend

* fix layout of chart cards

* tweak timeline chart labels

* timeline label tweak

* css tweak

* fix legend colors in statistics chart

* dark mode fix

* fix for y axis with a long name

* listen for darkMode changes and update charts

* legend tweak for darkMode

* dark mode tweak

* hide insignificant echarts errors for now
2025-01-28 14:20:34 +00:00
karwosts
e1bda9b57d Use fixed positioning for ha-form-multi_select (#23781) 2025-01-28 15:13:19 +01:00
ildar170975
89e9316a40 Fix typo & layout in ha-assist-chip (#23785)
* fix typo fot slot name

* make font-size same as for label

* remove unneeded margin-top
2025-01-28 15:04:12 +01:00
Paul Bottein
85e4975206 Use name property when formatting backup location (#23916) 2025-01-28 13:53:02 +01:00
Ben Randall
142faba04d Update settings button icon on tables to mdi-table-cog. (#23915)
Update settings button icon on tables to mdi-table-cog.

The cog icon is used in a lot of places to indicate settings and it's generally clear what it's intent is.  However, the cog icon used by tables very often appears very close by the triple dots menu or just in the top right corner so it's very easy to mistakenly press that button intending to press the menu button, or to press it when looking for the appropriate settings button.

This changes the icon used by these buttons from mdi-code to mdi-table-cog to help clarify the intent of the button.
2025-01-28 11:24:08 +02:00
karwosts
c1c6d71ccf Fix keyboard in automation-picker menus (#23867) 2025-01-28 09:39:03 +02:00
renovate[bot]
5204a565cf Update dependency eslint to v9.19.0 (#23914)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-28 09:37:55 +02:00
Petar Petrov
05b8a48ba8 Looser layout limits for graph cards (#23910) 2025-01-28 09:36:20 +02:00
renovate[bot]
41770a89b4 Update dependency element-internals-polyfill to v1.3.13 (#23912)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-27 16:46:14 +00:00
renovate[bot]
b7d14d7950 Update babel monorepo to v7.26.7 (#23911)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-27 17:34:34 +01:00
karwosts
4ccef6f28b Fix millisecond attributes (#23909)
fix millisecond attributes
2025-01-27 17:25:37 +02:00
karwosts
d1be441455 Re-remove time picker for Energy (#23891)
* Re-remove time picker for Energy

* flip polarity
2025-01-27 14:11:08 +00:00
Philipp
b3391b34e4 Increase generic entity row touch target (#23894) 2025-01-27 15:52:32 +02:00
Abílio Costa
9359e9d475 Reword incompatible media message (#23887) 2025-01-27 09:26:03 +02:00
Paul Bottein
5453da75ea Reintroduce backup switch when updating core and addons (#23814) 2025-01-27 08:25:28 +01:00
dependabot[bot]
bc21877008 Bump actions/stale from 9.0.0 to 9.1.0 (#23903)
Bumps [actions/stale](https://github.com/actions/stale) from 9.0.0 to 9.1.0.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v9.0.0...v9.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-27 09:24:34 +02:00
dependabot[bot]
c9956c65e9 Bump actions/setup-node from 4.1.0 to 4.2.0 (#23902)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.1.0...v4.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-27 09:24:01 +02:00
renovate[bot]
74814cc305 Update dependency @bundle-stats/plugin-webpack-filter to v4.18.2 (#23898)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-26 18:42:16 +00:00
renovate[bot]
8f231d7b3e Update rspack monorepo to v1.2.2 (#23896)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-26 19:33:12 +01:00
renovate[bot]
5b6818d72d Update dependency lint-staged to v15.4.2 (#23889)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-26 19:32:59 +01:00
renovate[bot]
8c75865a02 Update vitest monorepo to v3.0.4 (#23897)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-26 19:32:39 +01:00
J. Nick Koston
99d832ac77 Restore Bluetooth configuration panel (#23877)
* Restore Bluetooth configuration panel

* cleanup copypasta

* Apply suggestions from code review

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

* preen

* trim down updatePageEl
2025-01-26 08:11:21 +01:00
Simon Lamon
240e48f5c1 Include query params in url when default page is added (#23880)
Include query params if exists
2025-01-25 20:02:26 +01:00
Jan-Philipp Benecke
77fc11cda6 Use new improved save dialog when leaving script editor dirty (#23862)
* Use new improved save dialog when leaving script editor dirty

* Fix url
2025-01-25 19:14:47 +01:00
karwosts
f8f152f118 Add button filter to energy devices sortable (#23881)
Add filter to energy devices sortable
2025-01-25 18:42:05 +01:00
renovate[bot]
20a67cf73c Update rspack monorepo to v1.2.1 (#23884)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-25 17:45:34 +01:00
ocrease
dd11b3092e Preheating support in History Chart (#23878)
Use CLIMATE_HVAC_ACTION_TO_MODE to map hvac action to heat or cool so that history chart correctly shows preheating and defrosting as heat
2025-01-25 12:25:08 +01:00
renovate[bot]
7f3363621e Update dependency @bundle-stats/plugin-webpack-filter to v4.18.0 (#23879)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-25 12:20:46 +01:00
Jan-Philipp Benecke
edce1901d7 Display year when automation was last triggered in automation list (#23864)
* Display year when automation was last triggered in automation list

* Use function
2025-01-24 17:13:01 +00:00
Jan-Philipp Benecke
546087066a Display year in entities list only when last year (#23865)
* Display year in entities list only when last year

* Make function

* Revert hyphen
2025-01-24 18:03:21 +01:00
renovate[bot]
a95c589a06 Update vitest monorepo to v3.0.3 (#23870)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-24 17:59:35 +01:00
renovate[bot]
5f6da9d959 Update rspack monorepo to v1.2.0 (#23871)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-24 17:59:00 +01:00
renovate[bot]
bc94ca0a0a Pin dependency tslib to 2.8.1 (#23860)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-24 08:32:09 +02:00
Eric Stern
dc8d483e8b Logbook card loading fix (#23853)
* the call to unsubscribe will never do anything

* await the subscription when subscribing to logbook

* removed unneeded if
wrapped call to subscribeLogbook in try/catch
remove return values from _subscribeLogbookPeriod
2025-01-24 08:31:30 +02:00
renovate[bot]
a329553ce1 Update typescript-eslint monorepo to v8.21.0 (#23863)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-23 22:10:20 +01:00
Petar Petrov
1f8cfdd0de Migrate from chart.js to echarts (#23809) 2025-01-23 15:51:48 +00:00
Paul Bottein
fd1f966216 20250109.2 (#23859) 2025-01-23 16:18:56 +01:00
Paul Bottein
243a2ce0e2 Bumped version to 20250109.2 2025-01-23 16:16:42 +01:00
Paul Bottein
77c1786171 Fix delete button for state content in iOS and Android (#23847) 2025-01-23 16:15:49 +01:00
Paul Bottein
7a7c204d74 Fix delete button for state content in iOS (#23839) 2025-01-23 16:15:48 +01:00
Paul Bottein
dcb74ad2ee Fix backup data picker translations (#23826) 2025-01-23 16:15:47 +01:00
Paul Bottein
fd4c62a852 20250109.1 (#23858) 2025-01-23 14:26:04 +01:00
Paul Bottein
bf1b0ac949 Bumped version to 20250109.1 2025-01-23 14:17:53 +01:00
Bram Kragten
c6f0c62fd6 Prevent race in dialog box (#23758)
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-01-23 14:15:23 +01:00
Wendelin
ac98672cb7 Fix background (#23736) 2025-01-23 14:15:22 +01:00
Bram Kragten
6b471ba6e7 Fix background on cast devices (#23731) 2025-01-23 14:15:21 +01:00
Petar Petrov
b28e7d2f06 Fix navigation from stacked dialogs with the same name (#23698)
* Fix navigation from stacked dialogs

* lint fix

* Keep only 1 instance per dialog tag in the stack
2025-01-23 14:15:20 +01:00
Simon Lamon
bf471eb8c3 Minor fixes for backup translations (#23691) 2025-01-23 14:15:19 +01:00
Petar Petrov
8c52bc3ffb Fix more-info chart rendering (#23619)
* Fix more-info chart rendering

* lint fix

* remove animation-container & _chartHeight

* don't change height on resize

* handle default height in ha-chart-base

* fix chart height in energy panel

* lint

* lint
2025-01-23 14:12:54 +01:00
Bram Kragten
54b328648a Match UI with core and don't allow restore config without db and vice … (#23553)
Match UI with core and dont allow restore config without db and vice versa
2025-01-23 13:59:37 +01:00
Jan-Philipp Benecke
62a9da2de2 Display year for created_at and modified_at of entities (#23772) 2025-01-23 13:36:23 +02:00
renovate[bot]
25ff96b9bf Update dependency eslint-config-prettier to v10 (#23764)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-23 13:31:03 +02:00
Matthias Alphart
caeb616dc6 Don't check for promise when processing DataEntryFlowStep (#23759) 2025-01-23 13:27:19 +02:00
karwosts
fb79e2cfb2 Fix keyboard for ha-config-entities bulk menus (#23776)
Fix keyboard for ha-config-entities menus
2025-01-23 13:15:23 +02:00
Paul Bottein
666316e44a Use area entities in area card for temperature and humidity (#23842)
Use area entities in area card for temperature and humidity instead of average
2025-01-23 12:26:58 +02:00
karwosts
1532093426 Support offset on input_datetime time trigger (#23855)
* Support offset on input_datetime time trigger

* no time entities
2025-01-23 12:23:27 +02:00
Jan-Philipp Benecke
2effb0935c Add helper text to inputs of time pattern trigger (#23844) 2025-01-23 09:40:13 +02:00
renovate[bot]
06a08bb4f2 Update dependency intl-messageformat to v10.7.14 (#23854)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-23 09:04:21 +02:00
Jan-Philipp Benecke
09102d34d6 Improve automation save dialog when leaving editor dirty (#23589)
* Improve automation save dialog when leaving editor dirty

* Make CI happy
2025-01-23 09:03:39 +02:00
Wendelin
27d683f6e8 Add additional backup schedule description (#23843)
* Add additional backup schedule description

* Use ha-icon-button for description

* Remove tip style

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2025-01-22 20:17:50 +01:00
Paul Bottein
b89bd0be3b Fix delete button for state content in iOS and Android (#23847) 2025-01-22 14:25:06 +00:00
Paul Bottein
51e6e6d230 Fix delete button for state content in iOS (#23839) 2025-01-22 08:16:35 -05:00
Norbert Rittel
9005f8faa9 Update hidden_explanation to include labels (#23473) 2025-01-22 09:52:53 +01:00
Jan-Philipp Benecke
7aa2abc9c2 Make add integration dialog keyboard accessible (#23829)
Make add integration keyboard accessible
2025-01-22 10:06:05 +02:00
dependabot[bot]
898fd51c7d Bump vite from 6.0.7 to 6.0.11 (#23834)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.0.7 to 6.0.11.
- [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.0.11/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-22 10:04:27 +02:00
karwosts
b3141a0653 Remember selection "Don't Group" in data-tables (#23836) 2025-01-22 10:03:23 +02:00
Jan-Philipp Benecke
1025f73c36 Clear filter in add helper dialog when closing (#23832) 2025-01-21 21:40:52 +01:00
Simon Lamon
1cd44728df Temporarily disable Bluetooth panel to restore access to options flow, additional fixes (#23830) 2025-01-21 09:45:47 -10:00
Sören Beye
eaab19fb8c Don't hide location entities that are "home" in the MapViewStrategy (#23462) 2025-01-21 20:14:04 +02:00
Paul Bottein
c4b2896fac Add label for add badge button in masonry and sidebar view (#23827)
Add label for add badge button in mansonry and sidebar view
2025-01-21 19:45:06 +02:00
Simon Lamon
ca2a9f9171 Initial bluetooth integration panel (#23531)
* Initial bluetooth device page

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

Co-authored-by: J. Nick Koston <nick@koston.org>

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

* Apply suggestions from code review

* Apply suggestions and implement dialog

* memoize

* Apply suggestions

* Clean up and fixes

* Adjust store usage

* Implement hassdialog

* Apply comments

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2025-01-21 19:43:33 +02:00
Paulus Schoutsen
9e868e144d Allow storing temperature/humidity entities on an area (#23822)
* Allow storing temperature/humidity entities on an area

* Update objects after improved types
2025-01-21 17:37:51 +01:00
Bram Kragten
fcb6da55d8 Check if we can decrypt backup on download (#23756)
Co-authored-by: Kevin Cathcart <kevincathcart@gmail.com>
Co-authored-by: Wendelin <w@pe8.at>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-01-21 14:48:38 +01:00
Paul Bottein
87907b98bd Fix backup data picker translations (#23826) 2025-01-21 13:17:33 +01:00
Bram Kragten
7535d66373 Add time option for backup schedule (#23757)
Co-authored-by: Wendelin <w@pe8.at>
2025-01-21 11:58:07 +01:00
Jan-Philipp Benecke
e994e3565d Improve add cards dialog user experience (#23773)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-01-21 09:53:28 +01:00
Jan-Philipp Benecke
562589a6cb Fix flickering media play button (#23778) 2025-01-21 09:09:15 +01:00
renovate[bot]
e87f6d6d5e Update vitest monorepo to v3.0.2 (#23812)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 09:07:51 +01:00
karwosts
3feedb792a Multi textfield helper (#23649)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-01-20 12:20:36 +01:00
Douwe
fc290028c6 Update ha-assist-chat.ts (#23790)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-01-20 09:20:14 +01:00
Jan-Philipp Benecke
2f5fd6f0c7 Use destructive attribute from ha-button (#23786)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-01-20 08:54:22 +01:00
ildar170975
9a3ca59d77 Picture glance alignment fix (#23793) 2025-01-20 08:35:29 +01:00
ildar170975
f76c22cb5b system-log-card: make a header & card-content similar to error-log-card (#23799)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-01-20 08:26:18 +01:00
dependabot[bot]
aad94624e2 Bump release-drafter/release-drafter from 6.0.0 to 6.1.0 (#23806) 2025-01-20 08:15:29 +01:00
renovate[bot]
076ab91199 Update dependency intl-messageformat to v10.7.12 (#23807) 2025-01-20 08:12:59 +01:00
Jan-Philipp Benecke
d121b33263 Add shortcut hint to assist dialog (#23739)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-01-20 08:12:52 +01:00
renovate[bot]
dbfcf310c3 Update vitest monorepo to v3 (major) (#23803)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 08:05:24 +01:00
renovate[bot]
25ea6c2c55 Update dependency lint-staged to v15.4.1 (#23802)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-19 21:05:58 +01:00
renovate[bot]
b2b2c21477 Update dependency fs-extra to v11.3.0 (#23798)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-19 16:41:25 +01:00
renovate[bot]
09777db549 Update dependency lint-staged to v15.4.0 (#23800)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-19 16:41:13 +01:00
renovate[bot]
f108f65e39 Update dependency node-vibrant to v4.0.3 (#23797)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-19 16:41:04 +01:00
renovate[bot]
2ee9824bcc Update dependency node-vibrant to v4.0.2 (#23789)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-18 10:44:52 +00:00
Norbert Rittel
812461ea00 Add translatable string for "Learn how it works" (#23788)
* Add translatable string for "Learn how it works"

* Update assist-pref.ts adding localizable key
2025-01-18 11:37:22 +01:00
Jan-Philipp Benecke
c430c28c2a Add missing localization on info page (#23787) 2025-01-18 11:36:00 +01:00
Jan-Philipp Benecke
d67c463b98 Add clear button to Assist debug page (#23774)
* Add clear button to Assist debug page

* Move destructive to ha-button
2025-01-18 09:27:30 +02:00
Bram Kragten
3ffbd435e0 Merge branch 'rc' 2025-01-09 21:02:43 +01:00
Bram Kragten
2e2f39adbd Bumped version to 20250109.0 2025-01-09 21:02:30 +01:00
Paul Bottein
5e7f356707 Fix preferred agent for backup download (#23659) 2025-01-09 21:02:09 +01:00
Gord
b02d0e58b1 Backup text changes for the english translation (#23656)
* Update backup text in en.json

* fix missing quote en.json
2025-01-09 21:02:08 +01:00
Wendelin
c01d3aee41 Fix backup summary label position (#23655) 2025-01-09 21:02:07 +01:00
Paul Bottein
6dde7d6945 Fix backup translations key issues (#23654)
Co-authored-by: Wendelin <w@pe8.at>
2025-01-09 21:02:06 +01:00
Paul Bottein
14c71f436e Remove ! from backup translation (#23648) 2025-01-09 21:02:06 +01:00
Wendelin
9acdd9f903 Voice assistants config: Filter unavailable assists (#23637)
Filter unavailable assists from num of assist devices.
2025-01-09 21:02:05 +01:00
karwosts
12b2edaa65 Retain event data when moving/resizing schedule item (#23621)
* Retain event data when moving/resizing schedule item

* update from suggestion
2025-01-09 21:02:04 +01:00
Jan-Philipp Benecke
d55d388046 Set fixed width for automation save dialog (#23618) 2025-01-09 21:02:03 +01:00
Petar Petrov
ec1dedcb6b Fix tooltip scrolling (#23616) 2025-01-09 21:02:02 +01:00
Wendelin
d0fbba5063 Improve background-editor background-attachment alignment (#23615) 2025-01-09 21:02:01 +01:00
karwosts
344e083ac6 Restore attributes removed from ha-entity-marker in ha-map (#23603)
* Restore attributes removed from ha-entity-marker in ha-map

* Use Reflect
2025-01-09 21:02:00 +01:00
Wendelin
0ee6548650 Add backup translations (#23365)
* Add translations

* Add summary and progress translations

* Add backups and settings translations

* Add backups page translations

* Add onboarding card translations

* Add settings translations

* Add details translations

* Translate delete

* Add data picker translations

* Use local add-ons

* Add encryption key translations

* Add new, generate and upload translations

* Add translations for restore backup

* Fix ts issue

* Add missing keys

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2025-01-09 21:01:03 +01:00
Bram Kragten
9ff1ce3c96 Merge branch 'rc' 2025-01-06 18:13:49 +01:00
Norbert Rittel
f53ac94e76 Add missing ui.panel.config.labels.headers.description (#23517)
The header "Description" for the Labels list only shows up as optional in a narrow view like on mobile.

This commit adds the missing string for proper localization.
2025-01-06 18:13:32 +01:00
Norbert Rittel
6d084813d5 Add missing localizations for Voice Assistants > Expose headers (#23452)
* Add missing localizations for Voice Assistants > Expose headers

* Add localizable labels to Icon and Remove columns

* Variant with all changes on a single line

* Prettier?

* Revert

* Line length limited to 80 chars
2025-01-06 18:13:31 +01:00
Bram Kragten
926f5e3cd8 Merge branch 'rc' 2025-01-06 18:10:30 +01:00
Bram Kragten
befc650f81 Bumped version to 20250106.0 2025-01-06 18:10:12 +01:00
Bram Kragten
2019b8992e Fix tooltip more info (#23605) 2025-01-06 18:09:56 +01:00
Paul Bottein
4d2e9f203f Improve error handling in backup status banner (#23604)
* Improve error handling in backup status banner

* Fix completion

* Fix loading

* Check attempt and completion date first
2025-01-06 18:09:55 +01:00
Bram Kragten
760d898de7 Remove backup toggle from supervisor addon page when update available (#23602) 2025-01-06 18:09:06 +01:00
Ville Skyttä
a755af96a6 Spelling and grammar fixes (#23598) 2025-01-06 18:06:42 +01:00
Norbert Rittel
046b90ae25 Add localizable "Filtering by config entry" for Entities and Devices (#23544)
* Add localizable "Filtering by config entry" to en.json

This commit adds two strings for localizing "Filtering by config entry" to the Entities panel and, referenced from there, to the Devices panel.

* Replace "Filtering by config entry" with localizable key

* Replace "Filtering by config entry" with localizable key

* Add missing comma

* Add missing }

* Add missing }
2025-01-06 18:06:41 +01:00
Norbert Rittel
a3e8bcf848 Add ICU strings for proper singular / plural in Search fields (#23530)
It does not happen that often that the lists of devices, entities, helpers etc. are filtered down to a single item.

But in that case the labels currently use incorrect plural which is more irritating in some languages but also wrong in English.

This commit fixes this by adding ICU syntax to all six strings so these work properly in English and all derived translations.

For languages that need a different wording for `zero` this also helps translators in extending the ICU syntax for that case.
2025-01-06 18:05:49 +01:00
Petar Petrov
4b13dde92e Rename base sankey chart tag so it doesn't conflict with the custom card (#23600) 2025-01-06 18:00:38 +01:00
Bram Kragten
7aa2136c21 Merge branch 'rc' 2025-01-03 15:52:04 +01:00
Bram Kragten
47308e7b46 Add change of encryption key warning (#23570) 2025-01-03 15:51:39 +01:00
Bram Kragten
ec1fc09140 Bumped version to 20250103.0 2025-01-03 15:42:39 +01:00
Bram Kragten
e2ad94469a Fix restoring backup during onboarding (#23569) 2025-01-03 15:41:56 +01:00
Bram Kragten
70541ec966 Fix restore progress check logic (#23568) 2025-01-03 15:41:55 +01:00
Bram Kragten
0bf64ee7f4 Backup onboarding: Show close button when welcome is skipped (#23567) 2025-01-03 15:41:54 +01:00
Bram Kragten
4ea0c83fbe Close restore dialog if done (#23566) 2025-01-03 15:41:53 +01:00
Bram Kragten
d126e02747 fix error display upload backup (#23565) 2025-01-03 15:41:53 +01:00
Bram Kragten
102a9eeb61 Fix tabs subpage height on desktop (#23564)
fix tabs subpage height on desktop
2025-01-03 15:41:52 +01:00
Bram Kragten
fc3e99e794 Update and add backup my links (#23556)
* Update and add backup my links
2025-01-03 15:41:51 +01:00
Bram Kragten
70953788cc Add back zopfli compression (#23555) 2025-01-03 15:41:50 +01:00
Bram Kragten
6c9df587e7 Bumped version to 20250102.0 2025-01-02 16:18:11 +01:00
Bram Kragten
8f58681d83 always zoom timeline charts on x axis (#23554) 2025-01-02 16:17:04 +01:00
Bram Kragten
4a16d9bd44 Add show encryption key dialog (#23552) 2025-01-02 16:17:03 +01:00
Bram Kragten
fcc9da6d85 Backup with db requires config, disabled next if no data is selected (#23549) 2025-01-02 16:17:02 +01:00
Bram Kragten
e03dc2c382 Move local location backup setting (#23548) 2025-01-02 16:17:01 +01:00
Bram Kragten
be967940a2 Add warning when no backup location is selected (#23550)
* Add warning when no backup location is selected

* Move to bottom
2025-01-02 16:16:04 +01:00
Bram Kragten
64ad37ed6a Update change encryption key dialog (#23551) 2025-01-02 16:15:35 +01:00
Bram Kragten
01bc45c78b Backup text updates (#23547) 2025-01-02 16:13:39 +01:00
Marcin
2206644c47 Fix copy on button to clear the selected background image (#23546) 2025-01-02 16:13:38 +01:00
Bram Kragten
486038c426 Add space for the fab on datatable without tabs (#23545)
Add space for the fab on backups datatable
2025-01-02 16:13:38 +01:00
Sören Beye
711f721007 Changes to the valueText should also rescale ha-gauge text (#23536)
Changes to the valueText should also recenter ha-gauge text
2025-01-02 16:13:37 +01:00
Philipp
3b8bc242fe Fix media management delete button misalignment (#23534) 2025-01-02 16:13:36 +01:00
karwosts
7e80eed003 Display an error if saving new script times out (#23527)
* Display an error if saving new automation times out

* changes

* update

* string tweak

* Fix save failed for scripts
2025-01-02 16:13:35 +01:00
Bram Kragten
a7ef498d75 Handle no cloud subscription better in backups (#23523) 2025-01-02 16:13:34 +01:00
Bram Kragten
a5de6ff3af Bumped version to 20241231.0 2024-12-31 20:23:30 +01:00
Bram Kragten
806cc2c608 Fix automation traces (#23524) 2024-12-31 20:21:41 +01:00
Bram Kragten
48a160f057 Use last completed automatic backup time instead of last available ba… (#23522)
* Use last completed automatic backup time instead of last available backup

* Update ha-backup-overview-summary.ts

* Update src/panels/config/backup/components/overview/ha-backup-overview-summary.ts

* Update ha-config-backup-overview.ts
2024-12-31 20:21:40 +01:00
Bram Kragten
c697843c34 Update ha-backup-overview-summary.ts 2024-12-31 20:21:06 +01:00
Bram Kragten
317a2f5b21 Fix password incorrect check when restoring backup (#23525) 2024-12-31 20:18:15 +01:00
karwosts
220011f15f Display an error if saving new automation times out (#23518) 2024-12-31 20:16:53 +01:00
Bram Kragten
e8af454705 Bumped version to 20241230.0 2024-12-30 19:44:56 +01:00
Petar Petrov
713b5c7cf7 Add default border-radius values to .disabled-bar 2024-12-30 19:42:54 +01:00
Petar Petrov
8b17286fb6 Revert "Automation/Script editor border-radius fix (#23267)"
This reverts commit e9b2a83411.
2024-12-30 19:42:40 +01:00
Bram Kragten
f3705a7e1d Fix copy encryption key (#23515) 2024-12-30 19:41:01 +01:00
Bram Kragten
d0123b2cce Fix overflow of backup agents (#23514) 2024-12-30 19:41:00 +01:00
Bram Kragten
884c22f92b Add fallback for devices without name (#23513) 2024-12-30 19:40:59 +01:00
Simon Lamon
700690474c Add script hide picker again (#23512) 2024-12-30 19:40:58 +01:00
Simon Lamon
4686808e53 Fix manual backup disabled with all backup locations (#23511) 2024-12-30 19:40:57 +01:00
Jan-Philipp Benecke
cf1df712e4 Fix dialog header (#23507) 2024-12-30 19:40:56 +01:00
Timothy Kist
c338e9cb30 Remove space at end of link from HAOS storage tip (#23492) 2024-12-30 19:40:55 +01:00
Petar Petrov
8e8fd89d56 Fix custom DNS saving (#23477) 2024-12-30 19:40:55 +01:00
Petar Petrov
f1c360c550 Add getGridOptions to history and statistics graph cards (#23476) 2024-12-30 19:40:54 +01:00
Simon Lamon
b429ecc376 Calendar trigger: Handle optional offset better (#23474)
Calendar empty offset
2024-12-30 19:39:50 +01:00
Petar Petrov
6d8422513a Button to reset chart zoom (#23469) 2024-12-30 19:39:49 +01:00
Petar Petrov
cb0a48265a Fix helper dialog close and add failsafe for similar cases (#23468) 2024-12-30 19:39:48 +01:00
Simon Lamon
c9082724a8 View background settings: Change transparancy to opacity (#23450) 2024-12-30 19:39:47 +01:00
Paulus Schoutsen
fea83c0873 Bumped version to 20241229.0 2024-12-29 18:20:14 +00:00
karwosts
d3b4014182 Fix backups fab spacer (#23490) 2024-12-29 18:19:07 +00:00
Jan-Philipp Benecke
5c7fe04562 Fix header of config entry system options dialog (#23455)
Fix config entry system options dialog header
2024-12-29 18:19:06 +00:00
karwosts
44e26c925b Fix dialog-person-detail tracker selection (#23454) 2024-12-29 18:19:05 +00:00
Jan-Philipp Benecke
205dd3f968 Fix chip spacing in automation/script save dialog (#23451) 2024-12-29 18:19:04 +00:00
Paulus Schoutsen
86133a0696 Fix typo in backups overview (#23446) 2024-12-29 18:19:03 +00:00
Bram Kragten
2105db9104 change default of backup actions card feature to no backup (#23444) 2024-12-29 18:19:02 +00:00
Bram Kragten
c05054e4ff Merge branch 'rc' 2024-12-24 16:24:59 +01:00
Bram Kragten
f416b1b5da Merge branch 'dev' into rc 2024-12-24 16:23:46 +01:00
Bram Kragten
e7d9032cc4 20241223.1 (#23402) 2024-12-23 15:38:28 +01:00
Simon Lamon
331385794c View background settings: Change radio buttons to dropdowns (#23403) 2024-12-23 15:30:46 +01:00
Bram Kragten
a7cacbbbe6 20241223.0 (#23392) 2024-12-23 12:23:14 +01:00
228 changed files with 11759 additions and 7613 deletions

View File

@@ -11,6 +11,9 @@
"DEV_CONTAINER": "1",
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
},
"remoteEnv": {
"NODE_OPTIONS": "--max_old_space_size=8192"
},
"customizations": {
"vscode": {
"extensions": [

View File

@@ -26,7 +26,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -62,7 +62,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -26,7 +26,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -60,7 +60,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -78,7 +78,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -102,7 +102,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -27,7 +27,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -63,7 +63,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -18,6 +18,6 @@ jobs:
pull-requests: read
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6.0.0
- uses: release-drafter/release-drafter@v6.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -34,7 +34,7 @@ jobs:
uses: home-assistant/actions/helpers/verify-version@master
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -92,7 +92,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -121,7 +121,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
uses: actions/stale@v9.0.0
uses: actions/stale@v9.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90

View File

@@ -65,6 +65,7 @@ export class HaDemo extends HomeAssistantAppEl {
mockEntityRegistry(hass, [
{
config_entry_id: "co2signal",
config_subentry_id: null,
device_id: "co2signal",
area_id: null,
disabled_by: null,
@@ -85,6 +86,7 @@ export class HaDemo extends HomeAssistantAppEl {
},
{
config_entry_id: "co2signal",
config_subentry_id: null,
device_id: "co2signal",
area_id: null,
disabled_by: null,

View File

@@ -11,6 +11,7 @@ export const mockConfigEntries = (hass: MockHomeAssistant) => {
supports_remove_device: false,
supports_unload: true,
supports_reconfigure: true,
supported_subentry_types: {},
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,

View File

@@ -48,6 +48,7 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: "bedroom",
configuration_url: null,
config_entries: ["config_entry_1"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -71,6 +72,7 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: "backyard",
configuration_url: null,
config_entries: ["config_entry_2"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -94,6 +96,7 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: null,
configuration_url: null,
config_entries: ["config_entry_3"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -124,6 +127,8 @@ const AREAS: AreaRegistryEntry[] = [
picture: null,
aliases: [],
labels: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},
@@ -135,6 +140,8 @@ const AREAS: AreaRegistryEntry[] = [
picture: null,
aliases: [],
labels: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},
@@ -146,6 +153,8 @@ const AREAS: AreaRegistryEntry[] = [
picture: null,
aliases: [],
labels: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},

View File

@@ -47,6 +47,7 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: "bedroom",
configuration_url: null,
config_entries: ["config_entry_1"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -70,6 +71,7 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: "backyard",
configuration_url: null,
config_entries: ["config_entry_2"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -93,6 +95,7 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: null,
configuration_url: null,
config_entries: ["config_entry_3"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -123,6 +126,8 @@ const AREAS: AreaRegistryEntry[] = [
picture: null,
aliases: [],
labels: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},
@@ -134,6 +139,8 @@ const AREAS: AreaRegistryEntry[] = [
picture: null,
aliases: [],
labels: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},
@@ -145,6 +152,8 @@ const AREAS: AreaRegistryEntry[] = [
picture: null,
aliases: [],
labels: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},

View File

@@ -32,6 +32,8 @@ const createConfigEntry = (
supports_remove_device: false,
supports_unload: true,
supports_reconfigure: true,
supported_subentry_types: {},
num_subentries: 0,
disabled_by: null,
pref_disable_new_entities: false,
pref_disable_polling: false,
@@ -188,6 +190,7 @@ const createEntityRegistryEntries = (
): EntityRegistryEntry[] => [
{
config_entry_id: item.entry_id,
config_subentry_id: null,
device_id: "mock-device-id",
area_id: null,
disabled_by: null,
@@ -214,6 +217,7 @@ const createDeviceRegistryEntries = (
{
entry_type: null,
config_entries: [item.entry_id],
config_entries_subentries: {},
connections: [],
manufacturer: "ESPHome",
model: "Mock Device",

View File

@@ -1,8 +1,6 @@
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import { stripDiacritics } from "../../../src/common/string/strip-diacritics";
import type { StoreAddon } from "../../../src/data/supervisor/store";
import { getStripDiacriticsFn } from "../../../src/util/fuse";
export function filterAndSort(addons: StoreAddon[], filter: string) {
const options: IFuseOptions<StoreAddon> = {
@@ -10,8 +8,8 @@ export function filterAndSort(addons: StoreAddon[], filter: string) {
isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
getFn: getStripDiacriticsFn,
ignoreDiacritics: true,
};
const fuse = new Fuse(addons, options);
return fuse.search(stripDiacritics(filter)).map((result) => result.item);
return fuse.search(filter).map((result) => result.item);
}

View File

@@ -14,7 +14,7 @@ import type { LocalizeFunc } from "../../../src/common/translations/localize";
declare global {
interface HASSDomEvents {
"backup-uploaded": { backup: HassioBackup };
"hassio-backup-uploaded": { backup: HassioBackup };
"backup-cleared": undefined;
}
}
@@ -70,7 +70,7 @@ export class HassioUploadBackup extends LitElement {
this._uploading = true;
try {
const backup = await uploadBackup(this.hass, file);
fireEvent(this, "backup-uploaded", { backup: backup.data });
fireEvent(this, "hassio-backup-uploaded", { backup: backup.data });
} catch (err: any) {
showAlertDialog(this, {
title: "Upload failed",

View File

@@ -5,7 +5,6 @@ import { customElement, property, query } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { formatDate } from "../../../src/common/datetime/format_date";
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
import type { LocalizeFunc } from "../../../src/common/translations/localize";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-textfield";
@@ -19,13 +18,10 @@ import type {
} from "../../../src/data/hassio/backup";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
import type { HomeAssistant, TranslationDict } from "../../../src/types";
import type { HomeAssistant } from "../../../src/types";
import "./supervisor-formfield-label";
import type { HaTextField } from "../../../src/components/ha-textfield";
type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
interface CheckboxItem {
slug: string;
checked: boolean;
@@ -67,8 +63,6 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
export class SupervisorBackupContent extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public localize?: LocalizeFunc;
@property({ attribute: false }) public supervisor?: Supervisor;
@property({ attribute: false }) public backup?: HassioBackupDetail;
@@ -115,10 +109,6 @@ export class SupervisorBackupContent extends LitElement {
this._focusTarget?.focus();
}
private _localize = (key: BackupOrRestoreKey) =>
this.supervisor?.localize(`backup.${key}`) ||
this.localize!(`ui.panel.page-onboarding.restore.${key}`);
protected render() {
if (!this.onboarding && !this.supervisor) {
return nothing;
@@ -132,8 +122,8 @@ export class SupervisorBackupContent extends LitElement {
${this.backup
? html`<div class="details">
${this.backup.type === "full"
? this._localize("full_backup")
: this._localize("partial_backup")}
? this.supervisor?.localize("backup.full_backup")
: this.supervisor?.localize("backup.partial_backup")}
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
${this.hass
? formatDateTime(
@@ -145,7 +135,7 @@ export class SupervisorBackupContent extends LitElement {
</div>`
: html`<ha-textfield
name="backupName"
.label=${this._localize("name")}
.label=${this.supervisor?.localize("backup.name")}
.value=${this.backupName}
@change=${this._handleTextValueChanged}
>
@@ -153,11 +143,13 @@ export class SupervisorBackupContent extends LitElement {
${!this.backup || this.backup.type === "full"
? html`<div class="sub-header">
${!this.backup
? this._localize("type")
: this._localize("select_type")}
? this.supervisor?.localize("backup.type")
: this.supervisor?.localize("backup.select_type")}
</div>
<div class="backup-types">
<ha-formfield .label=${this._localize("full_backup")}>
<ha-formfield
.label=${this.supervisor?.localize("backup.full_backup")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
value="full"
@@ -166,7 +158,9 @@ export class SupervisorBackupContent extends LitElement {
>
</ha-radio>
</ha-formfield>
<ha-formfield .label=${this._localize("partial_backup")}>
<ha-formfield
.label=${this.supervisor?.localize("backup.partial_backup")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
value="partial"
@@ -202,7 +196,7 @@ export class SupervisorBackupContent extends LitElement {
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this._localize("folders")}
.label=${this.supervisor?.localize("backup.folders")}
.iconPath=${mdiFolder}
>
</supervisor-formfield-label>`}
@@ -222,7 +216,7 @@ export class SupervisorBackupContent extends LitElement {
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this._localize("addons")}
.label=${this.supervisor?.localize("backup.addons")}
.iconPath=${mdiPuzzle}
>
</supervisor-formfield-label>`}
@@ -247,7 +241,7 @@ export class SupervisorBackupContent extends LitElement {
${!this.backup
? html`<ha-formfield
class="password"
.label=${this._localize("password_protection")}
.label=${this.supervisor?.localize("backup.password_protection")}
>
<ha-checkbox
.checked=${this.backupHasPassword}
@@ -259,7 +253,7 @@ export class SupervisorBackupContent extends LitElement {
${this.backupHasPassword
? html`
<ha-password-field
.label=${this._localize("password")}
.label=${this.supervisor?.localize("backup.password")}
name="backupPassword"
.value=${this.backupPassword}
@change=${this._handleTextValueChanged}
@@ -267,7 +261,7 @@ export class SupervisorBackupContent extends LitElement {
</ha-password-field>
${!this.backup
? html`<ha-password-field
.label=${this._localize("confirm_password")}
.label=${this.supervisor?.localize("backup.confirm_password")}
name="confirmBackupPassword"
.value=${this.confirmBackupPassword}
@change=${this._handleTextValueChanged}

View File

@@ -72,7 +72,7 @@ export class DialogHassioBackupUpload
</ha-header-bar>
</div>
<hassio-upload-backup
@backup-uploaded=${this._backupUploaded}
@hassio-backup-uploaded=${this._backupUploaded}
.hass=${this.hass}
></hassio-upload-backup>
</ha-dialog>

View File

@@ -35,7 +35,6 @@ import { fileDownload } from "../../../../src/util/file_download";
import "../../components/supervisor-backup-content";
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
import type { BackupOrRestoreKey } from "../../util/translations";
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
@customElement("dialog-hassio-backup")
@@ -43,7 +42,7 @@ class HassioBackupDialog
extends LitElement
implements HassDialog<HassioBackupDialogParams>
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: string;
@@ -62,9 +61,13 @@ class HassioBackupDialog
this._dialogParams = dialogParams;
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
if (!this._backup) {
this._error = this._localize("no_backup_found");
this._error = this._dialogParams.supervisor?.localize(
"backup.no_backup_found"
);
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
this._error = this._localize("restore_no_home_assistant");
this._error = this._dialogParams.supervisor?.localize(
"backup.restore_no_home_assistant"
);
}
this._restoringBackup = false;
}
@@ -82,13 +85,6 @@ class HassioBackupDialog
return true;
}
private _localize(key: BackupOrRestoreKey) {
return (
this._dialogParams!.supervisor?.localize(`backup.${key}`) ||
this._dialogParams!.localize!(`ui.panel.page-onboarding.restore.${key}`)
);
}
protected render() {
if (!this._dialogParams || !this._backup) {
return nothing;
@@ -102,7 +98,7 @@ class HassioBackupDialog
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this._localize("close")}
.label=${this._dialogParams.supervisor?.localize("backup.close")}
.path=${mdiClose}
@click=${this.closeDialog}
.disabled=${this._restoringBackup}
@@ -150,7 +146,6 @@ class HassioBackupDialog
.supervisor=${this._dialogParams.supervisor}
.backup=${this._backup}
.onboarding=${this._dialogParams.onboarding || false}
.localize=${this._dialogParams.localize}
dialogInitialFocus
>
</supervisor-backup-content>
@@ -161,7 +156,7 @@ class HassioBackupDialog
.disabled=${this._restoringBackup || !!this._error}
@click=${this._restoreClicked}
>
${this._localize("restore")}
${this._dialogParams.supervisor?.localize("backup.restore")}
</ha-button>
</div>
</ha-md-dialog>
@@ -196,18 +191,22 @@ class HassioBackupDialog
}
if (
!(await showConfirmationDialog(this, {
title: this._localize(
this._backup!.type === "full"
? "confirm_restore_full_backup_title"
: "confirm_restore_partial_backup_title"
title: supervisor?.localize(
`backup.${
this._backup!.type === "full"
? "confirm_restore_full_backup_title"
: "confirm_restore_partial_backup_title"
}`
),
text: this._localize(
this._backup!.type === "full"
? "confirm_restore_full_backup_text"
: "confirm_restore_partial_backup_text"
text: supervisor?.localize(
`backup.${
this._backup!.type === "full"
? "confirm_restore_full_backup_text"
: "confirm_restore_partial_backup_text"
}`
),
confirmText: this._localize("restore"),
dismissText: this._localize("cancel"),
confirmText: supervisor?.localize("backup.restore"),
dismissText: supervisor?.localize("backup.cancel"),
}))
) {
this._restoringBackup = false;
@@ -227,7 +226,8 @@ class HassioBackupDialog
this.closeDialog();
} catch (error: any) {
this._error =
error?.body?.message || this._localize("restore_start_failed");
error?.body?.message ||
supervisor?.localize("backup.restore_start_failed");
} finally {
this._restoringBackup = false;
}
@@ -286,7 +286,7 @@ class HassioBackupDialog
title: supervisor.localize("backup.remote_download_title"),
text: supervisor.localize("backup.remote_download_text"),
confirmText: supervisor.localize("backup.download"),
dismissText: this._localize("cancel"),
dismissText: supervisor?.localize("backup.cancel"),
});
if (!confirm) {
return;
@@ -302,7 +302,7 @@ class HassioBackupDialog
private get _computeName() {
return this._backup
? this._backup.name || this._backup.slug
: this._localize("unnamed_backup");
: this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || "";
}
static get styles(): CSSResultGroup {

View File

@@ -1,5 +1,4 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { LocalizeFunc } from "../../../../src/common/translations/localize";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioBackupDialogParams {
@@ -8,7 +7,6 @@ export interface HassioBackupDialogParams {
onRestoring?: () => void;
onboarding?: boolean;
supervisor?: Supervisor;
localize?: LocalizeFunc;
}
export const showHassioBackupDialog = (

View File

@@ -9,6 +9,7 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
@@ -18,7 +19,11 @@ import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-faded";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown";
import "../../../src/components/ha-md-list";
import "../../../src/components/ha-md-list-item";
import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-switch";
import type { HaSwitch } from "../../../src/components/ha-switch";
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
import {
fetchHassioAddonChangelog,
@@ -121,6 +126,8 @@ class UpdateAvailableCard extends LitElement {
const changelog = changelogUrl(this._updateType, this._version_latest);
const createBackupTexts = this._computeCreateBackupTexts();
return html`
<ha-card
outlined
@@ -160,6 +167,30 @@ class UpdateAvailableCard extends LitElement {
)}
</p>
</div>
${createBackupTexts
? html`
<hr />
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${createBackupTexts.title}
</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
id="create-backup"
></ha-switch>
</ha-md-list-item>
</ha-md-list>
`
: nothing}
`
: html`<ha-circular-progress
aria-label="Updating"
@@ -227,6 +258,48 @@ class UpdateAvailableCard extends LitElement {
}
}
private _computeCreateBackupTexts():
| { title: string; description?: string }
| undefined {
// Addon backup
if (
this._updateType === "addon" &&
atLeastVersion(this.hass.config.version, 2025, 2, 0)
) {
const version = this._version;
return {
title: this.supervisor.localize("update_available.create_backup.addon"),
description: this.supervisor.localize(
"update_available.create_backup.addon_description",
{ version: version }
),
};
}
// Old behavior
if (this._updateType && ["core", "addon"].includes(this._updateType)) {
return {
title: this.supervisor.localize(
"update_available.create_backup.generic"
),
};
}
return undefined;
}
get _shouldCreateBackup(): boolean {
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
return false;
}
const createBackupSwitch = this.shadowRoot?.getElementById(
"create-backup"
) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
}
return true;
}
get _version(): string {
return this._updateType
? this._updateType === "addon"
@@ -341,14 +414,22 @@ class UpdateAvailableCard extends LitElement {
}
private async _update() {
if (this._shouldCreateBackup && this.supervisor.info.state === "freeze") {
this._error = this.supervisor.localize("backup.backup_already_running");
return;
}
this._error = undefined;
this._updating = true;
try {
if (this._updateType === "addon") {
await updateHassioAddon(this.hass, this.addonSlug!);
await updateHassioAddon(
this.hass,
this.addonSlug!,
this._shouldCreateBackup
);
} else if (this._updateType === "core") {
await updateCore(this.hass);
await updateCore(this.hass, this._shouldCreateBackup);
} else if (this._updateType === "os") {
await updateOS(this.hass);
} else if (this._updateType === "supervisor") {
@@ -403,6 +484,17 @@ class UpdateAvailableCard extends LitElement {
border-bottom: none;
margin: 16px 0 0 0;
}
ha-md-list {
padding: 0;
margin-bottom: -16px;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
}
`,
];
}

View File

@@ -1,4 +0,0 @@
import type { TranslationDict } from "../../../src/types";
export type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];

View File

@@ -26,14 +26,14 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.26.0",
"@babel/runtime": "7.26.7",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.4",
"@codemirror/commands": "6.8.0",
"@codemirror/language": "6.10.8",
"@codemirror/legacy-modes": "6.4.2",
"@codemirror/search": "6.5.8",
"@codemirror/state": "6.5.1",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.36.2",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.17.2",
@@ -91,16 +91,14 @@
"@polymer/polymer": "3.5.2",
"@replit/codemirror-indentation-markers": "6.5.3",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.6.2",
"@vaadin/vaadin-themable-mixin": "24.6.2",
"@vaadin/combo-box": "24.6.4",
"@vaadin/vaadin-themable-mixin": "24.6.4",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "2.3.1",
"chart.js": "4.4.7",
"chartjs-plugin-zoom": "2.2.0",
"barcode-detector": "3.0.0",
"color-name": "2.0.0",
"comlink": "4.4.2",
"core-js": "3.40.0",
@@ -110,14 +108,15 @@
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
"element-internals-polyfill": "1.3.12",
"fuse.js": "7.0.0",
"echarts": "5.6.0",
"element-internals-polyfill": "1.3.13",
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.7.11",
"intl-messageformat": "10.7.14",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -126,7 +125,7 @@
"luxon": "3.5.0",
"marked": "15.0.6",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.1",
"node-vibrant": "4.0.3",
"punycode": "2.3.1",
"qr-scanner": "1.4.2",
"qrcode": "1.5.4",
@@ -138,7 +137,7 @@
"tinykeys": "3.0.0",
"tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0",
"ua-parser-js": "2.0.0",
"ua-parser-js": "2.0.1",
"vis-data": "7.1.9",
"vis-network": "9.1.9",
"vue": "2.7.16",
@@ -153,22 +152,22 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.26.0",
"@babel/core": "7.26.7",
"@babel/helper-define-polyfill-provider": "0.6.3",
"@babel/plugin-proposal-decorators": "7.25.9",
"@babel/plugin-transform-runtime": "7.25.9",
"@babel/preset-env": "7.26.0",
"@babel/preset-env": "7.26.7",
"@babel/preset-typescript": "7.26.0",
"@bundle-stats/plugin-webpack-filter": "4.17.0",
"@bundle-stats/plugin-webpack-filter": "4.18.2",
"@lokalise/node-api": "13.0.0",
"@octokit/auth-oauth-device": "7.1.2",
"@octokit/plugin-retry": "7.1.3",
"@octokit/rest": "21.1.0",
"@rsdoctor/rspack-plugin": "0.4.13",
"@rspack/cli": "1.1.8",
"@rspack/core": "1.1.8",
"@rspack/cli": "1.2.2",
"@rspack/core": "1.2.2",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.20",
"@types/chromecast-caf-receiver": "6.0.21",
"@types/chromecast-caf-sender": "1.0.11",
"@types/color-name": "2.0.0",
"@types/glob": "8.1.0",
@@ -184,16 +183,16 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "8.20.0",
"@typescript-eslint/parser": "8.20.0",
"@vitest/coverage-v8": "2.1.8",
"@typescript-eslint/eslint-plugin": "8.23.0",
"@typescript-eslint/parser": "8.23.0",
"@vitest/coverage-v8": "3.0.5",
"babel-loader": "9.2.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.0",
"eslint": "9.18.0",
"eslint": "9.20.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "9.1.0",
"eslint-config-prettier": "10.0.1",
"eslint-import-resolver-webpack": "0.13.10",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-lit": "1.15.0",
@@ -201,7 +200,7 @@
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "2.2.0",
"fancy-log": "2.0.0",
"fs-extra": "11.2.0",
"fs-extra": "11.3.0",
"glob": "11.0.1",
"gulp": "5.0.0",
"gulp-brotli": "3.0.0",
@@ -211,7 +210,7 @@
"husky": "9.1.7",
"jsdom": "26.0.0",
"jszip": "3.10.1",
"lint-staged": "15.3.0",
"lint-staged": "15.4.3",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
@@ -220,12 +219,13 @@
"pinst": "3.0.0",
"prettier": "3.4.2",
"rspack-manifest-plugin": "5.0.3",
"serve": "14.2.4",
"sinon": "19.0.2",
"tar": "7.4.3",
"terser-webpack-plugin": "5.3.11",
"ts-lit-plugin": "2.0.2",
"typescript": "5.7.3",
"vitest": "2.1.8",
"vitest": "3.0.5",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
@@ -239,7 +239,8 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.15",
"globals": "15.14.0"
"globals": "15.14.0",
"tslib": "2.8.1"
},
"packageManager": "yarn@4.6.0"
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20241224.0"
version = "20250205.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@@ -64,7 +64,7 @@ echo Core is used from ${coreUrl}
HASS_URL="$coreUrl" ./script/develop &
# serve the frontend
yarn dlx serve -l $frontendPort ./hass_frontend -s &
./node_modules/.bin/serve -p $frontendPort --single --no-port-switching --config ../script/serve-config.json ./hass_frontend &
# keep the script running while serving
wait

3
script/serve-config.json Normal file
View File

@@ -0,0 +1,3 @@
{
"cleanUrls": false
}

View File

@@ -1,3 +1,4 @@
import memoizeOne from "memoize-one";
import { theme2hex } from "./convert-color";
export const COLORS = [
@@ -74,3 +75,12 @@ export function getGraphColorByIndex(
getColorByIndex(index);
return theme2hex(themeColor);
}
export const getAllGraphColors = memoizeOne(
(style: CSSStyleDeclaration) =>
COLORS.map((_color, index) => getGraphColorByIndex(index, style)),
(newArgs: [CSSStyleDeclaration], lastArgs: [CSSStyleDeclaration]) =>
// this is not ideal, but we need to memoize the colors
newArgs[0].getPropertyValue("--graph-color-1") ===
lastArgs[0].getPropertyValue("--graph-color-1")
);

View File

@@ -26,6 +26,20 @@ const formatDateTimeMem = memoizeOne(
})
);
export const formatDateTimeWithBrowserDefaults = (dateObj: Date) =>
formatDateTimeWithBrowserDefaultsMem().format(dateObj);
const formatDateTimeWithBrowserDefaultsMem = memoizeOne(
() =>
new Intl.DateTimeFormat(undefined, {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})
);
// Aug 9, 2021, 8:23 AM
export const formatShortDateTimeWithYear = (
dateObj: Date,
@@ -65,6 +79,18 @@ const formatShortDateTimeMem = memoizeOne(
})
);
export const formatShortDateTimeWithConditionalYear = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => {
const now = new Date();
if (now.getFullYear() === dateObj.getFullYear()) {
return formatShortDateTime(dateObj, locale, config);
}
return formatShortDateTimeWithYear(dateObj, locale, config);
};
// August 9, 2021, 8:23:15 AM
export const formatDateTimeWithSeconds = (
dateObj: Date,

View File

@@ -41,7 +41,7 @@ export class HaProgressButton extends LitElement {
indeterminate
></ha-circular-progress>
`
: ""}
: nothing}
</div>
`}
`;
@@ -117,6 +117,9 @@ export class HaProgressButton extends LitElement {
mwc-button.error slot {
visibility: hidden;
}
:host([destructive]) {
--mdc-theme-primary: var(--error-color);
}
`;
}

View File

@@ -0,0 +1,51 @@
import type { HassConfig } from "home-assistant-js-websocket";
import type { FrontendLocaleData } from "../../data/translation";
import {
formatDateMonth,
formatDateMonthYear,
formatDateVeryShort,
formatDateWeekdayShort,
} from "../../common/datetime/format_date";
import {
formatTime,
formatTimeWithSeconds,
} from "../../common/datetime/format_time";
export function formatTimeLabel(
value: number | Date,
locale: FrontendLocaleData,
config: HassConfig,
minutesDifference: number
) {
const dayDifference = minutesDifference / 60 / 24;
const date = new Date(value);
if (dayDifference > 88) {
return date.getMonth() === 0
? `{bold|${formatDateMonthYear(date, locale, config)}}`
: formatDateMonth(date, locale, config);
}
if (dayDifference > 35) {
return date.getDate() === 1
? `{bold|${formatDateVeryShort(date, locale, config)}}`
: formatDateVeryShort(date, locale, config);
}
if (dayDifference > 7) {
const label = formatDateVeryShort(date, locale, config);
return date.getDate() === 1 ? `{bold|${label}}` : label;
}
if (dayDifference > 2) {
return formatDateWeekdayShort(date, locale, config);
}
if (minutesDifference && minutesDifference < 5) {
return formatTimeWithSeconds(date, locale, config);
}
if (
date.getHours() === 0 &&
date.getMinutes() === 0 &&
date.getSeconds() === 0
) {
// show only date for the beginning of the day
return `{bold|${formatDateVeryShort(date, locale, config)}}`;
}
return formatTime(date, locale, config);
}

View File

@@ -1,269 +0,0 @@
import { _adapters } from "chart.js";
import {
startOfSecond,
startOfMinute,
startOfHour,
startOfDay,
startOfWeek,
startOfMonth,
startOfQuarter,
startOfYear,
addMilliseconds,
addSeconds,
addMinutes,
addHours,
addDays,
addWeeks,
addMonths,
addQuarters,
addYears,
differenceInMilliseconds,
differenceInSeconds,
differenceInMinutes,
differenceInHours,
differenceInDays,
differenceInWeeks,
differenceInMonths,
differenceInQuarters,
differenceInYears,
endOfSecond,
endOfMinute,
endOfHour,
endOfDay,
endOfWeek,
endOfMonth,
endOfQuarter,
endOfYear,
} from "date-fns";
import {
formatDate,
formatDateMonth,
formatDateMonthYear,
formatDateVeryShort,
formatDateWeekdayDay,
formatDateYear,
} from "../../common/datetime/format_date";
import {
formatDateTime,
formatDateTimeWithSeconds,
} from "../../common/datetime/format_date_time";
import {
formatTime,
formatTimeWithSeconds,
} from "../../common/datetime/format_time";
const FORMATS = {
datetime: "datetime",
datetimeseconds: "datetimeseconds",
millisecond: "millisecond",
second: "second",
minute: "minute",
hour: "hour",
day: "day",
date: "date",
weekday: "weekday",
week: "week",
month: "month",
monthyear: "monthyear",
quarter: "quarter",
year: "year",
};
_adapters._date.override({
formats: () => FORMATS,
parse: (value: Date | number) => {
if (!(value instanceof Date)) {
return value;
}
return value.getTime();
},
format: function (time, fmt: keyof typeof FORMATS) {
switch (fmt) {
case "datetime":
return formatDateTime(
new Date(time),
this.options.locale,
this.options.config
);
case "datetimeseconds":
return formatDateTimeWithSeconds(
new Date(time),
this.options.locale,
this.options.config
);
case "millisecond":
return formatTimeWithSeconds(
new Date(time),
this.options.locale,
this.options.config
);
case "second":
return formatTimeWithSeconds(
new Date(time),
this.options.locale,
this.options.config
);
case "minute":
return formatTime(
new Date(time),
this.options.locale,
this.options.config
);
case "hour":
return formatTime(
new Date(time),
this.options.locale,
this.options.config
);
case "weekday":
return formatDateWeekdayDay(
new Date(time),
this.options.locale,
this.options.config
);
case "date":
return formatDate(
new Date(time),
this.options.locale,
this.options.config
);
case "day":
return formatDateVeryShort(
new Date(time),
this.options.locale,
this.options.config
);
case "week":
return formatDateVeryShort(
new Date(time),
this.options.locale,
this.options.config
);
case "month":
return formatDateMonth(
new Date(time),
this.options.locale,
this.options.config
);
case "monthyear":
return formatDateMonthYear(
new Date(time),
this.options.locale,
this.options.config
);
case "quarter":
return formatDate(
new Date(time),
this.options.locale,
this.options.config
);
case "year":
return formatDateYear(
new Date(time),
this.options.locale,
this.options.config
);
default:
return "";
}
},
// @ts-ignore
add: (time, amount, unit) => {
switch (unit) {
case "millisecond":
return addMilliseconds(time, amount);
case "second":
return addSeconds(time, amount);
case "minute":
return addMinutes(time, amount);
case "hour":
return addHours(time, amount);
case "day":
return addDays(time, amount);
case "week":
return addWeeks(time, amount);
case "month":
return addMonths(time, amount);
case "quarter":
return addQuarters(time, amount);
case "year":
return addYears(time, amount);
default:
return time;
}
},
diff: (max, min, unit) => {
switch (unit) {
case "millisecond":
return differenceInMilliseconds(max, min);
case "second":
return differenceInSeconds(max, min);
case "minute":
return differenceInMinutes(max, min);
case "hour":
return differenceInHours(max, min);
case "day":
return differenceInDays(max, min);
case "week":
return differenceInWeeks(max, min);
case "month":
return differenceInMonths(max, min);
case "quarter":
return differenceInQuarters(max, min);
case "year":
return differenceInYears(max, min);
default:
return 0;
}
},
// @ts-ignore
startOf: (time, unit, weekday) => {
switch (unit) {
case "second":
return startOfSecond(time);
case "minute":
return startOfMinute(time);
case "hour":
return startOfHour(time);
case "day":
return startOfDay(time);
case "week":
return startOfWeek(time);
case "isoWeek":
return startOfWeek(time, {
weekStartsOn: +weekday! as 0 | 1 | 2 | 3 | 4 | 5 | 6,
});
case "month":
return startOfMonth(time);
case "quarter":
return startOfQuarter(time);
case "year":
return startOfYear(time);
default:
return time;
}
},
// @ts-ignore
endOf: (time, unit) => {
switch (unit) {
case "second":
return endOfSecond(time);
case "minute":
return endOfMinute(time);
case "hour":
return endOfHour(time);
case "day":
return endOfDay(time);
case "week":
return endOfWeek(time);
case "month":
return endOfMonth(time);
case "quarter":
return endOfQuarter(time);
case "year":
return endOfYear(time);
default:
return time;
}
},
});

View File

@@ -1,6 +0,0 @@
import type { ChartEvent } from "chart.js";
export const clickIsTouch = (event: ChartEvent): boolean =>
!(event.native instanceof MouseEvent) ||
(event.native instanceof PointerEvent &&
event.native.pointerType !== "mouse");

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ import { LitElement, html, css, svg, nothing } from "lit";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import memoizeOne from "memoize-one";
import type { HomeAssistant } from "../../types";
import { measureTextWidth } from "../../util/text";
export interface Node {
id: string;
@@ -68,15 +69,12 @@ export class HaSankeyChart extends LitElement {
private _statePerPixel = 0;
private _textMeasureCanvas?: HTMLCanvasElement;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect,
});
disconnectedCallback() {
super.disconnectedCallback();
this._textMeasureCanvas = undefined;
}
willUpdate() {
@@ -477,7 +475,7 @@ export class HaSankeyChart extends LitElement {
(node) =>
NODE_WIDTH +
TEXT_PADDING +
(node.label ? this._getTextWidth(node.label) : 0)
(node.label ? measureTextWidth(node.label, FONT_SIZE) : 0)
)
)
: 0;
@@ -492,18 +490,6 @@ export class HaSankeyChart extends LitElement {
: fullSize / nodesPerSection.length;
}
private _getTextWidth(text: string): number {
if (!this._textMeasureCanvas) {
this._textMeasureCanvas = document.createElement("canvas");
}
const context = this._textMeasureCanvas.getContext("2d");
if (!context) return 0;
// Match the font style from CSS
context.font = `${FONT_SIZE}px sans-serif`;
return context.measureText(text).width;
}
private _getVerticalLabelFontSize(label: string, labelWidth: number): number {
// reduce the label font size so the longest word fits on one line
const longestWord = label
@@ -513,7 +499,7 @@ export class HaSankeyChart extends LitElement {
longest.length > current.length ? longest : current,
""
);
const wordWidth = this._getTextWidth(longestWord);
const wordWidth = measureTextWidth(longestWord, FONT_SIZE);
return Math.min(FONT_SIZE, (labelWidth / wordWidth) * FONT_SIZE);
}

View File

@@ -1,19 +1,26 @@
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import type { PropertyValues } from "lit";
import { html, LitElement } from "lit";
import { property, state } from "lit/decorators";
import type { VisualMapComponentOption } from "echarts/components";
import type { LineSeriesOption } from "echarts/charts";
import type { YAXisOption } from "echarts/types/dist/shared";
import { styleMap } from "lit/directives/style-map";
import { getGraphColorByIndex } from "../../common/color/colors";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTL } from "../../common/util/compute_rtl";
import {
formatNumber,
numberFormatToLocale,
getNumberFormatOptions,
} from "../../common/number/format_number";
import type { LineChartEntity, LineChartState } from "../../data/history";
import type { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import { clickIsTouch } from "./click_is_touch";
import type { ECOption } from "../../resources/echarts";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import {
getNumberFormatOptions,
formatNumber,
} from "../../common/number/format_number";
import { measureTextWidth } from "../../util/text";
import { fireEvent } from "../../common/dom/fire_event";
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
const safeParseFloat = (value) => {
const parsed = parseFloat(value);
@@ -54,15 +61,19 @@ export class StateHistoryChartLine extends LitElement {
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
@state() private _chartData?: ChartData<"line">;
@property({ type: String }) public height?: string;
@state() private _chartData: LineSeriesOption[] = [];
@state() private _entityIds: string[] = [];
private _datasetToDataIndex: number[] = [];
@state() private _chartOptions?: ChartOptions;
@state() private _chartOptions?: ECOption;
@state() private _yWidth = 0;
private _hiddenStats = new Set<string>();
@state() private _yWidth = 25;
private _chartTime: Date = new Date();
@@ -72,171 +83,109 @@ export class StateHistoryChartLine extends LitElement {
.hass=${this.hass}
.data=${this._chartData}
.options=${this._chartOptions}
.paddingYAxis=${this.paddingYAxis - this._yWidth}
chart-type="line"
.height=${this.height}
style=${styleMap({ height: this.height })}
external-hidden
@dataset-hidden=${this._datasetHidden}
@dataset-unhidden=${this._datasetUnhidden}
></ha-chart-base>
`;
}
private _renderTooltip(params: any) {
const time = params[0].axisValue;
const title =
formatDateTimeWithSeconds(
new Date(time),
this.hass.locale,
this.hass.config
) + "<br>";
const datapoints: Record<string, any>[] = [];
this._chartData.forEach((dataset, index) => {
if (
dataset.tooltip?.show === false ||
this._hiddenStats.has(dataset.name as string)
)
return;
const param = params.find(
(p: Record<string, any>) => p.seriesIndex === index
);
if (param) {
datapoints.push(param);
return;
}
// If the datapoint is not found, we need to find the last datapoint before the current time
let lastData;
const data = dataset.data || [];
for (let i = data.length - 1; i >= 0; i--) {
const point = data[i];
if (point && point[0] <= time && point[1]) {
lastData = point;
break;
}
}
if (!lastData) return;
datapoints.push({
seriesName: dataset.name,
seriesIndex: index,
value: lastData,
// HTML copied from echarts. May change based on options
marker: `<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:${dataset.color};"></span>`,
});
});
const unit = this.unit
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
: "";
return (
title +
datapoints
.map((param) => {
const entityId = this._entityIds[param.seriesIndex];
const stateObj = this.hass.states[entityId];
const entry = this.hass.entities[entityId];
const stateValue = String(param.value[1]);
let value = stateObj
? this.hass.formatEntityState(stateObj, stateValue)
: `${formatNumber(
stateValue,
this.hass.locale,
getNumberFormatOptions(undefined, entry)
)}${unit}`;
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
const data = this.data[dataIndex];
if (data.statistics && data.statistics.length > 0) {
value += "<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
const source =
data.states.length === 0 ||
param.value[0] < data.states[0].last_changed
? `${this.hass.localize(
"ui.components.history_charts.source_stats"
)}`
: `${this.hass.localize(
"ui.components.history_charts.source_history"
)}`;
value += source;
}
if (param.seriesName) {
return `${param.marker} ${param.seriesName}: ${value}`;
}
return `${param.marker} ${value}`;
})
.join("<br>")
);
}
private _datasetHidden(ev: CustomEvent) {
this._hiddenStats.add(ev.detail.name);
}
private _datasetUnhidden(ev: CustomEvent) {
this._hiddenStats.delete(ev.detail.name);
}
public willUpdate(changedProps: PropertyValues) {
if (
!this.hasUpdated ||
changedProps.has("showNames") ||
changedProps.has("startTime") ||
changedProps.has("endTime") ||
changedProps.has("unit") ||
changedProps.has("logarithmicScale") ||
changedProps.has("minYAxis") ||
changedProps.has("maxYAxis") ||
changedProps.has("fitYData")
) {
this._chartOptions = {
parsing: false,
interaction: {
mode: "nearest",
axis: "xy",
},
scales: {
x: {
type: "time",
adapters: {
date: {
locale: this.hass.locale,
config: this.hass.config,
},
},
min: this.startTime,
max: this.endTime,
ticks: {
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
major: {
enabled: true,
},
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
: {},
},
time: {
tooltipFormat: "datetimeseconds",
},
},
y: {
suggestedMin: this.fitYData ? this.minYAxis : null,
suggestedMax: this.fitYData ? this.maxYAxis : null,
min: this.fitYData ? null : this.minYAxis,
max: this.fitYData ? null : this.maxYAxis,
ticks: {
maxTicksLimit: 7,
},
title: {
display: true,
text: this.unit,
},
afterUpdate: (y) => {
if (this._yWidth !== Math.floor(y.width)) {
this._yWidth = Math.floor(y.width);
fireEvent(this, "y-width-changed", {
value: this._yWidth,
chartIndex: this.chartIndex,
});
}
},
position: computeRTL(this.hass) ? "right" : "left",
type: this.logarithmicScale ? "logarithmic" : "linear",
},
},
plugins: {
tooltip: {
callbacks: {
label: (context) => {
let label = `${context.dataset.label}: ${formatNumber(
context.parsed.y,
this.hass.locale,
getNumberFormatOptions(
undefined,
this.hass.entities[this._entityIds[context.datasetIndex]]
)
)} ${this.unit}`;
const dataIndex =
this._datasetToDataIndex[context.datasetIndex];
const data = this.data[dataIndex];
if (data.statistics && data.statistics.length > 0) {
const source =
data.states.length === 0 ||
context.parsed.x < data.states[0].last_changed
? `\n${this.hass.localize(
"ui.components.history_charts.source_stats"
)}`
: `\n${this.hass.localize(
"ui.components.history_charts.source_history"
)}`;
label += source;
}
return label;
},
},
},
filler: {
propagate: true,
},
legend: {
display: this.showNames,
labels: {
usePointStyle: true,
},
},
},
elements: {
line: {
tension: 0.1,
borderWidth: 1.5,
},
point: {
hitRadius: 50,
},
},
segment: {
borderColor: (context) => {
// render stat data with a slightly transparent line
const dataIndex = this._datasetToDataIndex[context.datasetIndex];
const data = this.data[dataIndex];
return data.statistics &&
data.statistics.length > 0 &&
(data.states.length === 0 ||
context.p0.parsed.x < data.states[0].last_changed)
? this._chartData!.datasets[dataIndex].borderColor + "7F"
: undefined;
},
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => {
if (!this.clickForMoreInfo || clickIsTouch(e)) {
return;
}
const chart = e.chart;
const points = chart.getElementsAtEventForMode(
e,
"nearest",
{ intersect: true },
true
);
if (points.length) {
const firstPoint = points[0];
fireEvent(this, "hass-more-info", {
entityId: this._entityIds[firstPoint.datasetIndex],
});
chart.canvas.dispatchEvent(new Event("mouseout")); // to hide tooltip
}
},
};
}
if (
changedProps.has("data") ||
changedProps.has("startTime") ||
@@ -248,13 +197,130 @@ export class StateHistoryChartLine extends LitElement {
// so the X axis grows even if there is no new data
this._generateData();
}
if (
!this.hasUpdated ||
changedProps.has("showNames") ||
changedProps.has("startTime") ||
changedProps.has("endTime") ||
changedProps.has("unit") ||
changedProps.has("logarithmicScale") ||
changedProps.has("minYAxis") ||
changedProps.has("maxYAxis") ||
changedProps.has("fitYData") ||
changedProps.has("_chartData") ||
changedProps.has("paddingYAxis") ||
changedProps.has("_yWidth")
) {
const rtl = computeRTL(this.hass);
let minYAxis: number | ((values: { min: number }) => number) | undefined =
this.minYAxis;
let maxYAxis: number | ((values: { max: number }) => number) | undefined =
this.maxYAxis;
if (typeof minYAxis === "number") {
if (this.fitYData) {
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
}
} else if (this.logarithmicScale) {
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
}
if (typeof maxYAxis === "number") {
if (this.fitYData) {
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
}
} else if (this.logarithmicScale) {
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
}
this._chartOptions = {
xAxis: {
type: "time",
min: this.startTime,
max: this.endTime,
},
yAxis: {
type: this.logarithmicScale ? "log" : "value",
name: this.unit,
min: this._clampYAxis(minYAxis),
max: this._clampYAxis(maxYAxis),
position: rtl ? "right" : "left",
scale: true,
nameGap: 2,
nameTextStyle: {
align: "left",
},
axisLine: {
show: false,
},
axisLabel: {
margin: 5,
formatter: (value: number) => {
const label = formatNumber(value, this.hass.locale);
const width = measureTextWidth(label, 12) + 5;
if (width > this._yWidth) {
this._yWidth = width;
fireEvent(this, "y-width-changed", {
value: this._yWidth,
chartIndex: this.chartIndex,
});
}
return label;
},
},
} as YAXisOption,
legend: {
show: this.showNames,
type: "scroll",
animationDurationUpdate: 400,
icon: "circle",
padding: [20, 0],
},
grid: {
...(this.showNames ? {} : { top: 30 }), // undefined is the same as 0
left: rtl ? 1 : Math.max(this.paddingYAxis, this._yWidth),
right: rtl ? Math.max(this.paddingYAxis, this._yWidth) : 1,
bottom: 30,
},
visualMap: this._chartData
.map((_, seriesIndex) => {
const dataIndex = this._datasetToDataIndex[seriesIndex];
const data = this.data[dataIndex];
if (!data.statistics || data.statistics.length === 0) {
return false;
}
// render stat data with a slightly transparent line
const firstStateTS =
data.states[0]?.last_changed ?? this.endTime.getTime();
return {
show: false,
seriesIndex,
dimension: 0,
pieces: [
{
max: firstStateTS - 0.01,
colorAlpha: 0.5,
},
{
min: firstStateTS,
colorAlpha: 1,
},
],
};
})
.filter(Boolean) as VisualMapComponentOption[],
tooltip: {
trigger: "axis",
appendTo: document.body,
formatter: this._renderTooltip.bind(this),
},
};
}
}
private _generateData() {
let colorIndex = 0;
const computedStyles = getComputedStyle(this);
const entityStates = this.data;
const datasets: ChartDataset<"line">[] = [];
const datasets: LineSeriesOption[] = [];
const entityIds: string[] = [];
const datasetToDataIndex: number[] = [];
if (entityStates.length === 0) {
@@ -270,7 +336,7 @@ export class StateHistoryChartLine extends LitElement {
// array containing [value1, value2, etc]
let prevValues: any[] | null = null;
const data: ChartDataset<"line">[] = [];
const data: LineSeriesOption[] = [];
const pushData = (timestamp: Date, datavalues: any[] | null) => {
if (!datavalues) return;
@@ -287,26 +353,45 @@ export class StateHistoryChartLine extends LitElement {
// to the chart for the previous value. Otherwise the gap will
// be too big. It will go from the start of the previous data
// value until the start of the next data value.
d.data.push({ x: timestamp.getTime(), y: prevValues[i] });
d.data!.push([timestamp, prevValues[i]]);
}
d.data.push({ x: timestamp.getTime(), y: datavalues[i] });
d.data!.push([timestamp, datavalues[i]]);
});
prevValues = datavalues;
};
const addDataSet = (nameY: string, color?: string, fill = false) => {
const addDataSet = (
id: string,
nameY: string,
color?: string,
fill = false
) => {
if (!color) {
color = getGraphColorByIndex(colorIndex, computedStyles);
colorIndex++;
}
data.push({
label: nameY,
fill: fill ? "origin" : false,
borderColor: color,
backgroundColor: color + "7F",
stepped: "before",
pointRadius: 0,
id,
data: [],
type: "line",
cursor: "default",
name: nameY,
color,
symbol: "circle",
step: "end",
animationDurationUpdate: 0,
symbolSize: 1,
lineStyle: {
width: fill ? 0 : 1.5,
},
areaStyle: fill
? {
color: color + "7F",
}
: undefined,
tooltip: {
show: !fill,
},
});
entityIds.push(states.entity_id);
datasetToDataIndex.push(dataIdx);
@@ -324,12 +409,16 @@ export class StateHistoryChartLine extends LitElement {
const isHeating =
domain === "climate" && hasHvacAction
? (entityState: LineChartState) =>
entityState.attributes?.hvac_action === "heating"
CLIMATE_HVAC_ACTION_TO_MODE[
entityState.attributes?.hvac_action
] === "heat"
: (entityState: LineChartState) => entityState.state === "heat";
const isCooling =
domain === "climate" && hasHvacAction
? (entityState: LineChartState) =>
entityState.attributes?.hvac_action === "cooling"
CLIMATE_HVAC_ACTION_TO_MODE[
entityState.attributes?.hvac_action
] === "cool"
: (entityState: LineChartState) => entityState.state === "cool";
const hasHeat = states.states.some(isHeating);
@@ -345,13 +434,23 @@ export class StateHistoryChartLine extends LitElement {
entityState.attributes.target_temp_low
);
addDataSet(
`${this.hass.localize("ui.card.climate.current_temperature", {
name: name,
})}`
states.entity_id + "-current_temperature",
this.showNames
? this.hass.localize("ui.card.climate.current_temperature", {
name: name,
})
: this.hass.localize(
"component.climate.entity_component._.state_attributes.current_temperature.name"
)
);
if (hasHeat) {
addDataSet(
`${this.hass.localize("ui.card.climate.heating", { name: name })}`,
states.entity_id + "-heating",
this.showNames
? this.hass.localize("ui.card.climate.heating", { name: name })
: this.hass.localize(
"component.climate.entity_component._.state_attributes.hvac_action.state.heating"
),
computedStyles.getPropertyValue("--state-climate-heat-color"),
true
);
@@ -360,7 +459,12 @@ export class StateHistoryChartLine extends LitElement {
}
if (hasCool) {
addDataSet(
`${this.hass.localize("ui.card.climate.cooling", { name: name })}`,
states.entity_id + "-cooling",
this.showNames
? this.hass.localize("ui.card.climate.cooling", { name: name })
: this.hass.localize(
"component.climate.entity_component._.state_attributes.hvac_action.state.cooling"
),
computedStyles.getPropertyValue("--state-climate-cool-color"),
true
);
@@ -370,22 +474,40 @@ export class StateHistoryChartLine extends LitElement {
if (hasTargetRange) {
addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name,
mode: this.hass.localize("ui.card.climate.high"),
})}`
states.entity_id + "-target_temperature_mode",
this.showNames
? this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name,
mode: this.hass.localize("ui.card.climate.high"),
})
: this.hass.localize(
"component.climate.entity_component._.state_attributes.target_temp_high.name"
)
);
addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name,
mode: this.hass.localize("ui.card.climate.low"),
})}`
states.entity_id + "-target_temperature_mode_low",
this.showNames
? this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name,
mode: this.hass.localize("ui.card.climate.low"),
})
: this.hass.localize(
"component.climate.entity_component._.state_attributes.target_temp_low.name"
)
);
} else {
addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_entity", {
name: name,
})}`
states.entity_id + "-target_temperature",
this.showNames
? this.hass.localize(
"ui.card.climate.target_temperature_entity",
{
name: name,
}
)
: this.hass.localize(
"component.climate.entity_component._.state_attributes.temperature.name"
)
);
}
@@ -438,19 +560,29 @@ export class StateHistoryChartLine extends LitElement {
);
addDataSet(
`${this.hass.localize("ui.card.humidifier.target_humidity_entity", {
name: name,
})}`
states.entity_id + "-target_humidity",
this.showNames
? this.hass.localize("ui.card.humidifier.target_humidity_entity", {
name: name,
})
: this.hass.localize(
"component.humidifier.entity_component._.state_attributes.humidity.name"
)
);
if (hasCurrent) {
addDataSet(
`${this.hass.localize(
"ui.card.humidifier.current_humidity_entity",
{
name: name,
}
)}`
states.entity_id + "-current_humidity",
this.showNames
? this.hass.localize(
"ui.card.humidifier.current_humidity_entity",
{
name: name,
}
)
: this.hass.localize(
"component.humidifier.entity_component._.state_attributes.current_humidity.name"
)
);
}
@@ -458,25 +590,40 @@ export class StateHistoryChartLine extends LitElement {
// If action attribute is not available, we shade the area when the device is on
if (hasHumidifying) {
addDataSet(
`${this.hass.localize("ui.card.humidifier.humidifying", {
name: name,
})}`,
states.entity_id + "-humidifying",
this.showNames
? this.hass.localize("ui.card.humidifier.humidifying", {
name: name,
})
: this.hass.localize(
"component.humidifier.entity_component._.state_attributes.action.state.humidifying"
),
computedStyles.getPropertyValue("--state-humidifier-on-color"),
true
);
} else if (hasDrying) {
addDataSet(
`${this.hass.localize("ui.card.humidifier.drying", {
name: name,
})}`,
states.entity_id + "-drying",
this.showNames
? this.hass.localize("ui.card.humidifier.drying", {
name: name,
})
: this.hass.localize(
"component.humidifier.entity_component._.state_attributes.action.state.drying"
),
computedStyles.getPropertyValue("--state-humidifier-on-color"),
true
);
} else {
addDataSet(
`${this.hass.localize("ui.card.humidifier.on_entity", {
name: name,
})}`,
states.entity_id + "-on",
this.showNames
? this.hass.localize("ui.card.humidifier.on_entity", {
name: name,
})
: this.hass.localize(
"component.humidifier.entity_component._.state.on"
),
undefined,
true
);
@@ -509,7 +656,7 @@ export class StateHistoryChartLine extends LitElement {
pushData(new Date(entityState.last_changed), series);
});
} else {
addDataSet(name);
addDataSet(states.entity_id, name);
let lastValue: number;
let lastDate: Date;
@@ -575,12 +722,23 @@ export class StateHistoryChartLine extends LitElement {
Array.prototype.push.apply(datasets, data);
});
this._chartData = {
datasets,
};
this._chartData = datasets;
this._entityIds = entityIds;
this._datasetToDataIndex = datasetToDataIndex;
}
private _clampYAxis(value?: number | ((values: any) => number)) {
if (this.logarithmicScale) {
// log(0) is -Infinity, so we need to set a minimum value
if (typeof value === "number") {
return Math.max(value, 0.1);
}
if (typeof value === "function") {
return (values: any) => Math.max(value(values), 0.1);
}
}
return value;
}
}
customElements.define("state-history-chart-line", StateHistoryChartLine);

View File

@@ -1,19 +1,26 @@
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { getRelativePosition } from "chart.js/helpers";
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import type {
CustomSeriesOption,
CustomSeriesRenderItem,
ECElementEvent,
TooltipFormatterCallback,
TooltipPositionCallbackParams,
} from "echarts/types/dist/shared";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
import { fireEvent } from "../../common/dom/fire_event";
import { numberFormatToLocale } from "../../common/number/format_number";
import { computeRTL } from "../../common/util/compute_rtl";
import type { TimelineEntity } from "../../data/history";
import type { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import type { TimeLineData } from "./timeline-chart/const";
import { computeTimelineColor } from "./timeline-chart/timeline-color";
import { clickIsTouch } from "./click_is_touch";
import { computeTimelineColor } from "./timeline-color";
import type { ECOption } from "../../resources/echarts";
import echarts from "../../resources/echarts";
import { luminosity } from "../../common/color/rgb";
import { hex2rgb } from "../../common/color/convert-color";
import { measureTextWidth } from "../../util/text";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("state-history-chart-timeline")
export class StateHistoryChartTimeline extends LitElement {
@@ -44,9 +51,9 @@ export class StateHistoryChartTimeline extends LitElement {
@property({ attribute: false, type: Number }) public chartIndex?;
@state() private _chartData?: ChartData<"timeline">;
@state() private _chartData: CustomSeriesOption[] = [];
@state() private _chartOptions?: ChartOptions<"timeline">;
@state() private _chartOptions?: ECOption;
@state() private _yWidth = 0;
@@ -56,20 +63,99 @@ export class StateHistoryChartTimeline extends LitElement {
return html`
<ha-chart-base
.hass=${this.hass}
.data=${this._chartData}
.options=${this._chartOptions}
.height=${this.data.length * 30 + 30}
.paddingYAxis=${this.paddingYAxis - this._yWidth}
chart-type="timeline"
.height=${`${this.data.length * 30 + 30}px`}
.data=${this._chartData as ECOption["series"]}
@chart-click=${this._handleChartClick}
></ha-chart-base>
`;
}
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this._createOptions();
private _renderItem: CustomSeriesRenderItem = (params, api) => {
const categoryIndex = api.value(0);
const start = api.coord([api.value(1), categoryIndex]);
const end = api.coord([api.value(2), categoryIndex]);
const height = 20;
const coordSys = params.coordSys as any;
const rectShape = echarts.graphic.clipRectByRect(
{
x: start[0],
y: start[1] - height / 2,
width: end[0] - start[0],
height: height,
},
{
x: coordSys.x,
y: coordSys.y,
width: coordSys.width,
height: coordSys.height,
}
);
if (!rectShape) return null;
const rect = {
type: "rect" as const,
transition: "shape" as const,
shape: rectShape,
style: {
fill: api.value(4) as string,
},
};
const text = api.value(3) as string;
const textWidth = measureTextWidth(text, 12);
const LABEL_PADDING = 4;
if (textWidth < rectShape.width - LABEL_PADDING * 2) {
return {
type: "group",
children: [
rect,
{
type: "text",
style: {
...rectShape,
x: rectShape.x + LABEL_PADDING,
text,
fill: api.value(5) as string,
fontSize: 12,
lineHeight: rectShape.height,
},
},
],
};
}
return rect;
};
private _renderTooltip: TooltipFormatterCallback<TooltipPositionCallbackParams> =
(params: TooltipPositionCallbackParams) => {
const { value, name, marker, seriesName } = Array.isArray(params)
? params[0]
: params;
const title = seriesName
? `<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
: "";
const durationInMs = value![2] - value![1];
const formattedDuration = `${this.hass.localize(
"ui.components.history_charts.duration"
)}: ${millisecondsToDuration(durationInMs)}`;
const lines = [
marker + name,
formatDateTimeWithSeconds(
new Date(value![1]),
this.hass.locale,
this.hass.config
),
formatDateTimeWithSeconds(
new Date(value![2]),
this.hass.locale,
this.hass.config
),
formattedDuration,
].join("<br>");
return [title, lines].join("");
};
public willUpdate(changedProps: PropertyValues) {
if (
changedProps.has("startTime") ||
changedProps.has("endTime") ||
@@ -83,9 +169,12 @@ export class StateHistoryChartTimeline extends LitElement {
}
if (
!this.hasUpdated ||
changedProps.has("startTime") ||
changedProps.has("endTime") ||
changedProps.has("showNames")
changedProps.has("showNames") ||
changedProps.has("paddingYAxis") ||
changedProps.has("_yWidth")
) {
this._createOptions();
}
@@ -93,144 +182,71 @@ export class StateHistoryChartTimeline extends LitElement {
private _createOptions() {
const narrow = this.narrow;
const showNames = this.chunked || this.showNames;
const maxInternalLabelWidth = narrow ? 105 : 185;
const labelWidth = showNames
? Math.max(this.paddingYAxis, this._yWidth)
: 0;
const labelMargin = 5;
const rtl = computeRTL(this.hass);
this._chartOptions = {
maintainAspectRatio: false,
parsing: false,
scales: {
x: {
type: "time",
position: "bottom",
adapters: {
date: {
locale: this.hass.locale,
config: this.hass.config,
},
},
min: this.startTime,
suggestedMax: this.endTime,
ticks: {
autoSkip: true,
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
major: {
enabled: true,
},
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
: {},
},
grid: {
offset: false,
},
time: {
tooltipFormat: "datetimeseconds",
},
xAxis: {
type: "time",
min: this.startTime,
max: this.endTime,
axisTick: {
show: true,
},
y: {
type: "category",
barThickness: 20,
offset: true,
grid: {
display: false,
drawBorder: false,
drawTicks: false,
},
ticks: {
display: this.chunked || this.showNames,
},
afterSetDimensions: (y) => {
y.maxWidth = y.chart.width * 0.18;
},
afterFit: (scaleInstance) => {
if (this.chunked) {
// ensure all the chart labels are the same width
scaleInstance.width = narrow ? 105 : 185;
}
},
afterUpdate: (y) => {
const yWidth = this.showNames
? (y.width ?? 0)
: computeRTL(this.hass)
? 0
: (y.left ?? 0);
if (
this._yWidth !== Math.floor(yWidth) &&
y.ticks.length === this.data.length
) {
this._yWidth = Math.floor(yWidth);
splitLine: {
show: false,
},
},
yAxis: {
type: "category",
inverse: true,
position: rtl ? "right" : "left",
triggerEvent: true,
axisTick: {
show: false,
},
axisLine: {
show: false,
},
axisLabel: {
show: showNames,
width: labelWidth,
overflow: "truncate",
margin: labelMargin,
formatter: (id: string) => {
const label = this._chartData.find((d) => d.id === id)
?.name as string;
const width = label
? Math.min(
measureTextWidth(label, 12) + labelMargin,
maxInternalLabelWidth
)
: 0;
if (width > this._yWidth) {
this._yWidth = width;
fireEvent(this, "y-width-changed", {
value: this._yWidth,
chartIndex: this.chartIndex,
});
}
return label;
},
position: computeRTL(this.hass) ? "right" : "left",
hideOverlap: true,
},
},
plugins: {
tooltip: {
mode: "nearest",
callbacks: {
title: (context) =>
context![0].chart!.data!.labels![
context[0].datasetIndex
] as string,
beforeBody: (context) => context[0].dataset.label || "",
label: (item) => {
const d = item.dataset.data[item.dataIndex] as TimeLineData;
const durationInMs = d.end.getTime() - d.start.getTime();
const formattedDuration = `${this.hass.localize(
"ui.components.history_charts.duration"
)}: ${millisecondsToDuration(durationInMs)}`;
return [
d.label || "",
formatDateTimeWithSeconds(
d.start,
this.hass.locale,
this.hass.config
),
formatDateTimeWithSeconds(
d.end,
this.hass.locale,
this.hass.config
),
formattedDuration,
];
},
labelColor: (item) => ({
borderColor: (item.dataset.data[item.dataIndex] as TimeLineData)
.color!,
backgroundColor: (
item.dataset.data[item.dataIndex] as TimeLineData
).color!,
}),
},
},
filler: {
propagate: true,
},
grid: {
top: 10,
bottom: 30,
left: rtl ? 1 : labelWidth,
right: rtl ? labelWidth : 1,
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => {
if (!this.clickForMoreInfo || clickIsTouch(e)) {
return;
}
const chart = e.chart;
const canvasPosition = getRelativePosition(e, chart);
const index = Math.abs(
chart.scales.y.getValueForPixel(canvasPosition.y)
);
fireEvent(this, "hass-more-info", {
// @ts-ignore
entityId: this._chartData?.datasets[index]?.label,
});
chart.canvas.dispatchEvent(new Event("mouseout")); // to hide tooltip
tooltip: {
appendTo: document.body,
formatter: this._renderTooltip,
},
};
}
@@ -246,8 +262,7 @@ export class StateHistoryChartTimeline extends LitElement {
this._chartTime = new Date();
const startTime = this.startTime;
const endTime = this.endTime;
const labels: string[] = [];
const datasets: ChartDataset<"timeline">[] = [];
const datasets: CustomSeriesOption[] = [];
const names = this.names || {};
// stateHistory is a list of lists of sorted state objects
stateHistory.forEach((stateInfo) => {
@@ -255,10 +270,11 @@ export class StateHistoryChartTimeline extends LitElement {
let prevState: string | null = null;
let locState: string | null = null;
let prevLastChanged = startTime;
const entityDisplay: string =
names[stateInfo.entity_id] || stateInfo.name;
const entityDisplay: string = this.showNames
? names[stateInfo.entity_id] || stateInfo.name || stateInfo.entity_id
: "";
const dataRow: TimeLineData[] = [];
const dataRow: unknown[] = [];
stateInfo.data.forEach((entityState) => {
let newState: string | null = entityState.state;
const timeStamp = new Date(entityState.last_changed);
@@ -277,15 +293,23 @@ export class StateHistoryChartTimeline extends LitElement {
} else if (newState !== prevState) {
newLastChanged = new Date(entityState.last_changed);
const color = computeTimelineColor(
prevState,
computedStyles,
this.hass.states[stateInfo.entity_id]
);
dataRow.push({
start: prevLastChanged,
end: newLastChanged,
label: locState,
color: computeTimelineColor(
prevState,
computedStyles,
this.hass.states[stateInfo.entity_id]
),
value: [
stateInfo.entity_id,
prevLastChanged,
newLastChanged,
locState,
color,
luminosity(hex2rgb(color)) > 0.5 ? "#000" : "#fff",
],
itemStyle: {
color,
},
});
prevState = newState;
@@ -295,28 +319,52 @@ export class StateHistoryChartTimeline extends LitElement {
});
if (prevState !== null) {
const color = computeTimelineColor(
prevState,
computedStyles,
this.hass.states[stateInfo.entity_id]
);
dataRow.push({
start: prevLastChanged,
end: endTime,
label: locState,
color: computeTimelineColor(
prevState,
computedStyles,
this.hass.states[stateInfo.entity_id]
),
value: [
stateInfo.entity_id,
prevLastChanged,
endTime,
locState,
color,
luminosity(hex2rgb(color)) > 0.5 ? "#000" : "#fff",
],
itemStyle: {
color,
},
});
}
datasets.push({
id: stateInfo.entity_id,
data: dataRow,
label: stateInfo.entity_id,
name: entityDisplay,
dimensions: ["id", "start", "end", "name", "color", "textColor"],
type: "custom",
encode: {
x: [1, 2],
y: 0,
itemName: 3,
},
renderItem: this._renderItem,
});
labels.push(entityDisplay);
});
this._chartData = {
labels: labels,
datasets: datasets,
};
this._chartData = datasets;
}
private _handleChartClick(e: CustomEvent<ECElementEvent>): void {
if (e.detail.targetType === "axisLabel") {
const dataset = this._chartData[e.detail.dataIndex];
if (dataset) {
fireEvent(this, "hass-more-info", {
entityId: dataset.id as string,
});
}
}
}
static styles = css`

View File

@@ -69,6 +69,8 @@ export class StateHistoryCharts extends LitElement {
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
@property({ type: String }) public height?: string;
private _computedStartTime!: Date;
private _computedEndTime!: Date;
@@ -133,7 +135,7 @@ export class StateHistoryCharts extends LitElement {
return html``;
}
if (!Array.isArray(item)) {
return html`<div class="entry-container">
return html`<div class="entry-container line">
<state-history-chart-line
.hass=${this.hass}
.unit=${item.unit}
@@ -151,10 +153,11 @@ export class StateHistoryCharts extends LitElement {
.maxYAxis=${this.maxYAxis}
.fitYData=${this.fitYData}
@y-width-changed=${this._yWidthChanged}
.height=${this.virtualize ? undefined : this.height}
></state-history-chart-line>
</div> `;
}
return html`<div class="entry-container">
return html`<div class="entry-container timeline">
<state-history-chart-timeline
.hass=${this.hass}
.data=${item}
@@ -274,7 +277,8 @@ export class StateHistoryCharts extends LitElement {
static styles = css`
:host {
display: block;
display: flex;
flex-direction: column;
/* height of single timeline chart = 60px */
min-height: 60px;
}
@@ -297,6 +301,10 @@ export class StateHistoryCharts extends LitElement {
width: 100%;
}
.entry-container.line {
flex: 1;
}
.entry-container:hover {
z-index: 1;
}
@@ -308,6 +316,10 @@ export class StateHistoryCharts extends LitElement {
padding-inline-end: 1px;
}
.entry-container.timeline:first-child {
margin-top: var(--timeline-top-margin);
}
.entry-container:not(:first-child) {
border-top: 2px solid var(--divider-color);
margin-top: 16px;

View File

@@ -1,21 +1,22 @@
import type {
ChartData,
ChartDataset,
ChartOptions,
ChartType,
} from "chart.js";
BarSeriesOption,
LineSeriesOption,
} from "echarts/types/dist/shared";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { getGraphColorByIndex } from "../../common/color/colors";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import {
formatNumber,
numberFormatToLocale,
getNumberFormatOptions,
} from "../../common/number/format_number";
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
import { computeRTL } from "../../common/util/compute_rtl";
import type {
Statistics,
StatisticsMetaData,
@@ -25,13 +26,11 @@ import {
getDisplayUnit,
getStatisticLabel,
getStatisticMetadata,
isExternalStatistic,
statisticsHaveType,
} from "../../data/recorder";
import type { ECOption } from "../../resources/echarts";
import type { HomeAssistant } from "../../types";
import "./ha-chart-base";
import type { ChartDatasetExtra } from "./ha-chart-base";
import { clickIsTouch } from "./click_is_touch";
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
mean: "mean",
@@ -57,12 +56,14 @@ export class StatisticsChart extends LitElement {
@property() public unit?: string;
@property({ attribute: false }) public startTime?: Date;
@property({ attribute: false }) public endTime?: Date;
@property({ attribute: false, type: Array })
public statTypes: StatisticType[] = ["sum", "min", "mean", "max"];
@property({ attribute: false }) public chartType: ChartType = "line";
@property({ attribute: false }) public chartType: "line" | "bar" = "line";
@property({ attribute: false, type: Number }) public minYAxis?: number;
@@ -84,13 +85,18 @@ export class StatisticsChart extends LitElement {
@property() public period?: string;
@state() private _chartData: ChartData = { datasets: [] };
@property({ attribute: "days-to-show", type: Number })
public daysToShow?: number;
@state() private _chartDatasetExtra: ChartDatasetExtra[] = [];
@property({ type: String }) public height?: string;
@state() private _chartData: (LineSeriesOption | BarSeriesOption)[] = [];
@state() private _legendData: string[] = [];
@state() private _statisticIds: string[] = [];
@state() private _chartOptions?: ChartOptions;
@state() private _chartOptions?: ECOption;
@state() private _hiddenStats = new Set<string>();
@@ -101,8 +107,14 @@ export class StatisticsChart extends LitElement {
}
public willUpdate(changedProps: PropertyValues) {
if (changedProps.has("legendMode")) {
this._hiddenStats.clear();
if (
changedProps.has("statisticsData") ||
changedProps.has("statTypes") ||
changedProps.has("chartType") ||
changedProps.has("hideLegend") ||
changedProps.has("_hiddenStats")
) {
this._generateData();
}
if (
!this.hasUpdated ||
@@ -113,19 +125,14 @@ export class StatisticsChart extends LitElement {
changedProps.has("maxYAxis") ||
changedProps.has("fitYData") ||
changedProps.has("logarithmicScale") ||
changedProps.has("hideLegend")
changedProps.has("hideLegend") ||
changedProps.has("startTime") ||
changedProps.has("endTime") ||
changedProps.has("_legendData") ||
changedProps.has("_chartData")
) {
this._createOptions();
}
if (
changedProps.has("statisticsData") ||
changedProps.has("statTypes") ||
changedProps.has("chartType") ||
changedProps.has("hideLegend") ||
changedProps.has("_hiddenStats")
) {
this._generateData();
}
}
public firstUpdated() {
@@ -157,145 +164,158 @@ export class StatisticsChart extends LitElement {
return html`
<ha-chart-base
external-hidden
.hass=${this.hass}
.data=${this._chartData}
.extraData=${this._chartDatasetExtra}
.options=${this._chartOptions}
.chartType=${this.chartType}
.height=${this.height}
style=${styleMap({ height: this.height })}
external-hidden
@dataset-hidden=${this._datasetHidden}
@dataset-unhidden=${this._datasetUnhidden}
></ha-chart-base>
`;
}
private _datasetHidden(ev) {
ev.stopPropagation();
this._hiddenStats.add(this._statisticIds[ev.detail.index]);
private _datasetHidden(ev: CustomEvent) {
this._hiddenStats.add(ev.detail.name);
this.requestUpdate("_hiddenStats");
}
private _datasetUnhidden(ev) {
ev.stopPropagation();
this._hiddenStats.delete(this._statisticIds[ev.detail.index]);
private _datasetUnhidden(ev: CustomEvent) {
this._hiddenStats.delete(ev.detail.name);
this.requestUpdate("_hiddenStats");
}
private _createOptions(unit?: string) {
this._chartOptions = {
parsing: false,
interaction: {
mode: "nearest",
axis: "x",
},
scales: {
x: {
type: "time",
adapters: {
date: {
locale: this.hass.locale,
config: this.hass.config,
},
},
ticks: {
source: this.chartType === "bar" ? "data" : undefined,
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
major: {
enabled: true,
},
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
: {},
},
time: {
tooltipFormat: "datetime",
unit:
this.chartType === "bar" &&
this.period &&
["hour", "day", "week", "month"].includes(this.period)
? this.period
: undefined,
},
},
y: {
beginAtZero: this.chartType === "bar",
ticks: {
maxTicksLimit: 7,
},
title: {
display: unit || this.unit,
text: unit || this.unit,
},
type: this.logarithmicScale ? "logarithmic" : "linear",
min: this.fitYData ? null : this.minYAxis,
max: this.fitYData ? null : this.maxYAxis,
},
},
plugins: {
tooltip: {
callbacks: {
label: (context) =>
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
private _renderTooltip = (params: any) => {
const rendered: Record<string, boolean> = {};
const unit = this.unit
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
: "";
return params
.map((param, index: number) => {
if (rendered[param.seriesName]) return "";
rendered[param.seriesName] = true;
const statisticId = this._statisticIds[param.seriesIndex];
const stateObj = this.hass.states[statisticId];
const entry = this.hass.entities[statisticId];
// max series can have 3 values, as the second value is the max-min to form a band
const rawValue = String(param.value[2] ?? param.value[1]);
const options = getNumberFormatOptions(stateObj, entry) ?? {
maximumFractionDigits: 2,
};
const value = `${formatNumber(
rawValue,
this.hass.locale,
options
)}${unit}`;
const time =
index === 0
? formatDateTimeWithSeconds(
new Date(param.value[0]),
this.hass.locale,
getNumberFormatOptions(
undefined,
this.hass.entities[this._statisticIds[context.datasetIndex]]
)
)} ${
// @ts-ignore
context.dataset.unit || ""
}`,
},
},
filler: {
propagate: true,
},
legend: {
display: !this.hideLegend,
labels: {
usePointStyle: true,
},
},
},
elements: {
line: {
tension: 0.4,
cubicInterpolationMode: "monotone",
borderWidth: 1.5,
},
bar: { borderWidth: 1.5, borderRadius: 4 },
point: {
hitRadius: 50,
},
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => {
if (!this.clickForMoreInfo || clickIsTouch(e)) {
return;
this.hass.config
) + "<br>"
: "";
return `${time}${param.marker} ${param.seriesName}: ${value}`;
})
.filter(Boolean)
.join("<br>");
};
private _createOptions() {
const dayDifference = this.daysToShow ?? 1;
let minYAxis: number | ((values: { min: number }) => number) | undefined =
this.minYAxis;
let maxYAxis: number | ((values: { max: number }) => number) | undefined =
this.maxYAxis;
if (typeof minYAxis === "number") {
if (this.fitYData) {
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
}
} else if (this.logarithmicScale) {
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
}
if (typeof maxYAxis === "number") {
if (this.fitYData) {
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
}
} else if (this.logarithmicScale) {
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
}
const endTime = this.endTime ?? new Date();
let startTime = this.startTime;
if (!startTime) {
// set start time to the earliest point in the chart data
this._chartData.forEach((series) => {
if (!Array.isArray(series.data) || !series.data[0]) return;
const firstPoint = series.data[0] as any;
const timestamp = Array.isArray(firstPoint)
? firstPoint[0]
: firstPoint.value?.[0];
if (timestamp && (!startTime || new Date(timestamp) < startTime)) {
startTime = new Date(timestamp);
}
});
const chart = e.chart;
const points = chart.getElementsAtEventForMode(
e,
"nearest",
{ intersect: true },
true
if (!startTime) {
// Calculate default start time based on dayDifference
startTime = new Date(
endTime.getTime() - dayDifference * 24 * 3600 * 1000
);
}
}
if (points.length) {
const firstPoint = points[0];
const statisticId = this._statisticIds[firstPoint.datasetIndex];
if (!isExternalStatistic(statisticId)) {
fireEvent(this, "hass-more-info", { entityId: statisticId });
chart.canvas.dispatchEvent(new Event("mouseout")); // to hide tooltip
}
}
this._chartOptions = {
xAxis: [
{
type: "time",
min: startTime,
max: endTime,
},
{
type: "time",
show: false,
},
],
yAxis: {
type: this.logarithmicScale ? "log" : "value",
name: this.unit,
nameGap: 2,
nameTextStyle: {
align: "left",
},
position: computeRTL(this.hass) ? "right" : "left",
scale: true,
min: this._clampYAxis(minYAxis),
max: this._clampYAxis(maxYAxis),
splitLine: {
show: true,
},
},
legend: {
show: !this.hideLegend,
type: "scroll",
animationDurationUpdate: 400,
icon: "circle",
padding: [20, 0],
data: this._legendData,
},
grid: {
...(this.hideLegend ? { top: this.unit ? 30 : 5 } : {}), // undefined is the same as 0
left: 1,
right: 1,
bottom: 0,
containLabel: true,
},
tooltip: {
trigger: "axis",
appendTo: document.body,
formatter: this._renderTooltip,
},
};
}
@@ -325,8 +345,8 @@ export class StatisticsChart extends LitElement {
let colorIndex = 0;
const statisticsData = Object.entries(this.statisticsData);
const totalDataSets: ChartDataset<"line">[] = [];
const totalDatasetExtras: ChartDatasetExtra[] = [];
const totalDataSets: typeof this._chartData = [];
const legendData: { name: string; color: string }[] = [];
const statisticIds: string[] = [];
let endTime: Date;
@@ -348,6 +368,7 @@ export class StatisticsChart extends LitElement {
if (endTime > new Date()) {
endTime = new Date();
}
this.endTime = endTime;
let unit: string | undefined | null;
@@ -372,19 +393,19 @@ export class StatisticsChart extends LitElement {
}
// array containing [value1, value2, etc]
let prevValues: (number | null)[] | null = null;
let prevValues: (number | null)[][] | null = null;
let prevEndTime: Date | undefined;
// The datasets for the current statistic
const statDataSets: ChartDataset<"line">[] = [];
const statDatasetExtras: ChartDatasetExtra[] = [];
const statDataSets: (LineSeriesOption | BarSeriesOption)[] = [];
const statLegendData: { name: string; color: string }[] = [];
const pushData = (
start: Date,
end: Date,
dataValues: (number | null)[] | null
dataValues: (number | null)[][]
) => {
if (!dataValues) return;
if (!dataValues.length) return;
if (start > end) {
// Drop data points that are after the requested endTime. This could happen if
// endTime is "now" and client time is not in sync with server time.
@@ -399,11 +420,12 @@ export class StatisticsChart extends LitElement {
) {
// if the end of the previous data doesn't match the start of the current data,
// we have to draw a gap so add a value at the end time, and then an empty value.
d.data.push({ x: prevEndTime.getTime(), y: prevValues[i]! });
// @ts-expect-error
d.data.push({ x: prevEndTime.getTime(), y: null });
d.data!.push(
this._transformDataValue([prevEndTime, ...prevValues[i]!])
);
d.data!.push([prevEndTime, null]);
}
d.data.push({ x: start.getTime(), y: dataValues[i]! });
d.data!.push(this._transformDataValue([start, ...dataValues[i]!]));
});
prevValues = dataValues;
prevEndTime = end;
@@ -438,49 +460,64 @@ export class StatisticsChart extends LitElement {
})
: this.statTypes;
let displayed_legend = false;
let displayedLegend = false;
sortedTypes.forEach((type) => {
if (statisticsHaveType(stats, type)) {
const band = drawBands && (type === "min" || type === "max");
if (!this.hideLegend) {
const show_legend = hasMean
const showLegend = hasMean
? type === "mean"
: displayed_legend === false;
statDatasetExtras.push({
legend_label: name,
show_legend,
});
displayed_legend = displayed_legend || show_legend;
: displayedLegend === false;
if (showLegend) {
statLegendData.push({ name, color });
}
displayedLegend = displayedLegend || showLegend;
}
statTypes.push(type);
statDataSets.push({
label: name
const borderColor =
band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color;
const backgroundColor = band ? color + "3F" : color + "7F";
const series: LineSeriesOption | BarSeriesOption = {
id: `${statistic_id}-${type}`,
type: this.chartType,
smooth: this.chartType === "line" ? 0.4 : false,
smoothMonotone: "x",
cursor: "default",
data: [],
name: name
? `${name} (${this.hass.localize(
`ui.components.statistics_charts.statistic_types.${type}`
)})
`
)})`
: this.hass.localize(
`ui.components.statistics_charts.statistic_types.${type}`
),
fill: drawBands
? type === "min" && hasMean
? "+1"
: type === "max"
? "-1"
: false
: false,
borderColor:
band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color,
backgroundColor: band ? color + "3F" : color + "7F",
pointRadius: 0,
hidden: !this.hideLegend
? this._hiddenStats.has(statistic_id)
: false,
data: [],
// @ts-ignore
unit: meta?.unit_of_measurement,
band,
});
symbol: "circle",
symbolSize: 0,
animationDurationUpdate: 0,
lineStyle: {
width: 1.5,
},
itemStyle:
this.chartType === "bar"
? {
borderRadius: [4, 4, 0, 0],
borderColor,
borderWidth: 1.5,
}
: undefined,
color: this.chartType === "bar" ? backgroundColor : borderColor,
};
if (band && this.chartType === "line") {
series.stack = `band-${statistic_id}`;
series.stackStrategy = "all";
(series as LineSeriesOption).symbol = "none";
if (drawBands && type === "max") {
(series as LineSeriesOption).areaStyle = {
color: color + "3F",
};
}
}
statDataSets.push(series);
statisticIds.push(statistic_id);
}
});
@@ -494,40 +531,79 @@ export class StatisticsChart extends LitElement {
return;
}
prevDate = startDate;
const dataValues: (number | null)[] = [];
const dataValues: (number | null)[][] = [];
statTypes.forEach((type) => {
let val: number | null | undefined;
const val: (number | null)[] = [];
if (type === "sum") {
if (firstSum === null || firstSum === undefined) {
val = 0;
val.push(0);
firstSum = stat.sum;
} else {
val = (stat.sum || 0) - firstSum;
val.push((stat.sum || 0) - firstSum);
}
} else if (type === "max" && this.chartType === "line") {
const max = stat.max || 0;
val.push(Math.abs(max - (stat.min || 0)));
val.push(max);
} else {
val = stat[type];
val.push(stat[type] ?? null);
}
dataValues.push(val ?? null);
dataValues.push(val);
});
pushData(startDate, new Date(stat.end), dataValues);
if (!this._hiddenStats.has(name)) {
pushData(startDate, new Date(stat.end), dataValues);
}
});
// Concat two arrays
Array.prototype.push.apply(totalDataSets, statDataSets);
Array.prototype.push.apply(totalDatasetExtras, statDatasetExtras);
Array.prototype.push.apply(legendData, statLegendData);
});
if (unit) {
this._createOptions(unit);
this.unit = unit;
}
this._chartData = {
datasets: totalDataSets,
};
this._chartDatasetExtra = totalDatasetExtras;
legendData.forEach(({ name, color }) => {
// Add an empty series for the legend
totalDataSets.push({
id: name + "-legend",
name: name,
color,
type: this.chartType,
data: [],
xAxisIndex: 1,
});
});
this._chartData = totalDataSets;
if (legendData.length !== this._legendData.length) {
// only update the legend if it has changed or it will trigger options update
this._legendData = legendData.map(({ name }) => name);
}
this._statisticIds = statisticIds;
}
private _transformDataValue(val: [Date, ...(number | null)[]]) {
if (this.chartType === "bar" && val[1] && val[1] < 0) {
return { value: val, itemStyle: { borderRadius: [0, 0, 4, 4] } };
}
return val;
}
private _clampYAxis(value?: number | ((values: any) => number)) {
if (this.logarithmicScale) {
// log(0) is -Infinity, so we need to set a minimum value
if (typeof value === "number") {
return Math.max(value, 0.1);
}
if (typeof value === "function") {
return (values: any) => Math.max(value(values), 0.1);
}
}
return value;
}
static styles = css`
:host {
display: block;

View File

@@ -1,22 +0,0 @@
import type {
BarControllerChartOptions,
BarControllerDatasetOptions,
} from "chart.js";
export interface TimeLineData {
start: Date;
end: Date;
label?: string | null;
color?: string;
}
declare module "chart.js" {
interface ChartTypeRegistry {
timeline: {
chartOptions: BarControllerChartOptions;
datasetOptions: BarControllerDatasetOptions;
defaultDataPoint: TimeLineData;
parsedDataType: any;
};
}
}

View File

@@ -1,63 +0,0 @@
import type { BarOptions, BarProps } from "chart.js";
import { BarElement } from "chart.js";
import { hex2rgb } from "../../../common/color/convert-color";
import { luminosity } from "../../../common/color/rgb";
export interface TextBarProps extends BarProps {
text?: string | null;
options?: Partial<TextBaroptions>;
}
export interface TextBaroptions extends BarOptions {
textPad?: number;
textColor?: string;
backgroundColor: string;
}
export class TextBarElement extends BarElement {
static id = "textbar";
draw(ctx: CanvasRenderingContext2D) {
super.draw(ctx);
const options = this.options as TextBaroptions;
const { x, y, base, width, text } = (
this as BarElement<TextBarProps, TextBaroptions>
).getProps(["x", "y", "base", "width", "text"]);
if (!text) {
return;
}
ctx.beginPath();
const textRect = ctx.measureText(text);
if (
textRect.width === 0 ||
textRect.width + (options.textPad || 4) + 2 > width
) {
return;
}
const textColor =
options.textColor ||
(options?.backgroundColor === "transparent"
? "transparent"
: luminosity(hex2rgb(options.backgroundColor)) > 0.5
? "#000"
: "#fff");
// ctx.font = "12px arial";
ctx.fillStyle = textColor;
ctx.lineWidth = 0;
ctx.strokeStyle = textColor;
ctx.textBaseline = "middle";
ctx.fillText(
text,
x - width / 2 + (options.textPad || 4),
y + (base - y) / 2
);
}
tooltipPosition(useFinalPosition: boolean) {
const { x, y, base } = this.getProps(["x", "y", "base"], useFinalPosition);
return { x, y: y + (base - y) / 2 };
}
}

View File

@@ -1,255 +0,0 @@
import type { BarElement } from "chart.js";
import { BarController } from "chart.js";
import type { TimeLineData } from "./const";
import type { TextBarProps } from "./textbar-element";
function borderProps(properties) {
let reverse;
let start;
let end;
let top;
let bottom;
if (properties.horizontal) {
reverse = properties.base > properties.x;
start = "left";
end = "right";
} else {
reverse = properties.base < properties.y;
start = "bottom";
end = "top";
}
if (reverse) {
top = "end";
bottom = "start";
} else {
top = "start";
bottom = "end";
}
return { start, end, reverse, top, bottom };
}
function setBorderSkipped(properties, options, stack, index) {
let edge = options.borderSkipped;
const res = {};
if (!edge) {
properties.borderSkipped = res;
return;
}
if (edge === true) {
properties.borderSkipped = {
top: true,
right: true,
bottom: true,
left: true,
};
return;
}
const { start, end, reverse, top, bottom } = borderProps(properties);
if (edge === "middle" && stack) {
properties.enableBorderRadius = true;
if ((stack._top || 0) === index) {
edge = top;
} else if ((stack._bottom || 0) === index) {
edge = bottom;
} else {
res[parseEdge(bottom, start, end, reverse)] = true;
edge = top;
}
}
res[parseEdge(edge, start, end, reverse)] = true;
properties.borderSkipped = res;
}
function parseEdge(edge, a, b, reverse) {
if (reverse) {
edge = swap(edge, a, b);
edge = startEnd(edge, b, a);
} else {
edge = startEnd(edge, a, b);
}
return edge;
}
function swap(orig, v1, v2) {
return orig === v1 ? v2 : orig === v2 ? v1 : orig;
}
function startEnd(v, start, end) {
return v === "start" ? start : v === "end" ? end : v;
}
function setInflateAmount(
properties,
{ inflateAmount }: { inflateAmount?: string | number },
ratio
) {
properties.inflateAmount =
inflateAmount === "auto" ? (ratio === 1 ? 0.33 : 0) : inflateAmount;
}
function parseValue(entry, item, vScale, i) {
const startValue = vScale.parse(entry.start, i);
const endValue = vScale.parse(entry.end, i);
const min = Math.min(startValue, endValue);
const max = Math.max(startValue, endValue);
let barStart = min;
let barEnd = max;
if (Math.abs(min) > Math.abs(max)) {
barStart = max;
barEnd = min;
}
// Store `barEnd` (furthest away from origin) as parsed value,
// to make stacking straight forward
item[vScale.axis] = barEnd;
item._custom = {
barStart,
barEnd,
start: startValue,
end: endValue,
min,
max,
};
return item;
}
export class TimelineController extends BarController {
static id = "timeline";
static defaults = {
dataElementType: "textbar",
dataElementOptions: ["text", "textColor", "textPadding"],
elements: {
showText: true,
textPadding: 4,
minBarWidth: 1,
},
layout: {
padding: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
},
};
static overrides = {
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
};
parseObjectData(meta, data, start, count) {
const iScale = meta.iScale;
const vScale = meta.vScale;
const labels = iScale.getLabels();
const singleScale = iScale === vScale;
const parsed: any[] = [];
let i;
let ilen;
let item;
let entry;
for (i = start, ilen = start + count; i < ilen; ++i) {
entry = data[i];
item = {};
item[iScale.axis] = singleScale || iScale.parse(labels[i], i);
parsed.push(parseValue(entry, item, vScale, i));
}
return parsed;
}
getLabelAndValue(index) {
const meta = this._cachedMeta;
const { vScale } = meta;
const data = this.getDataset().data[index] as TimeLineData;
return {
label: vScale!.getLabelForValue(this.index) || "",
value: data.label || "",
};
}
updateElements(
bars: BarElement[],
start: number,
count: number,
mode: "reset" | "resize" | "none" | "hide" | "show" | "default" | "active"
) {
const vScale = this._cachedMeta.vScale!;
const iScale = this._cachedMeta.iScale!;
const dataset = this.getDataset();
const firstOpts = this.resolveDataElementOptions(start, mode);
const sharedOptions = this.getSharedOptions(firstOpts);
const includeOptions = this.includeOptions(mode, sharedOptions!);
const horizontal = vScale.isHorizontal();
this.updateSharedOptions(sharedOptions!, mode, firstOpts);
for (let index = start; index < start + count; index++) {
const data = dataset.data[index] as TimeLineData;
const y = vScale.getPixelForValue(this.index);
const xStart = iScale.getPixelForValue(
Math.max(iScale.min, data.start.getTime())
);
const xEnd = iScale.getPixelForValue(data.end.getTime());
const width = xEnd - xStart;
const parsed = this.getParsed(index);
const stack = (parsed._stacks || {})[vScale.axis];
const height = 10;
const properties: TextBarProps = {
horizontal,
x: xStart + width / 2, // Center of the bar
y: y - height, // Top of bar
width,
height: 0,
base: y + height, // Bottom of bar,
// Text
text: data.label,
};
if (includeOptions) {
properties.options =
sharedOptions || this.resolveDataElementOptions(index, mode);
properties.options = {
...properties.options,
backgroundColor: data.color,
};
}
const options = properties.options || bars[index].options;
setBorderSkipped(properties, options, stack, index);
setInflateAmount(properties, options, 1);
this.updateElement(bars[index], index, properties as any, mode);
}
}
removeHoverStyle(_element, _datasetIndex, _index) {
// this._setStyle(element, index, 'active', false);
}
setHoverStyle(_element, _datasetIndex, _index) {
// this._setStyle(element, index, 'active', true);
}
}

View File

@@ -1,11 +1,11 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { getGraphColorByIndex } from "../../../common/color/colors";
import { hex2rgb, lab2hex, rgb2lab } from "../../../common/color/convert-color";
import { labBrighten } from "../../../common/color/lab";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorProperties } from "../../../common/entity/state_color";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity";
import { computeCssValue } from "../../../resources/css-variables";
import { getGraphColorByIndex } from "../../common/color/colors";
import { hex2rgb, lab2hex, rgb2lab } from "../../common/color/convert-color";
import { labBrighten } from "../../common/color/lab";
import { computeDomain } from "../../common/entity/compute_domain";
import { stateColorProperties } from "../../common/entity/state_color";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { computeCssValue } from "../../resources/css-variables";
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
media_player: {

View File

@@ -33,9 +33,10 @@ export class HaAssistChip extends MdAssistChip {
}
/** Set the size of mdc icons **/
::slotted([slot="icon"]),
::slotted([slot="trailingIcon"]) {
::slotted([slot="trailing-icon"]) {
display: flex;
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
font-size: var(--_label-text-size) !important;
}
.trailing.icon ::slotted(*),

View File

@@ -178,7 +178,7 @@ class HaEntityStatePicker extends LitElement {
no-style
@item-moved=${this._moveItem}
.disabled=${this.disabled}
filter="button.trailing.action"
handle-selector="button.primary.action"
>
<ha-chip-set>
${repeat(
@@ -195,12 +195,7 @@ class HaEntityStatePicker extends LitElement {
.label=${label}
selected
>
<ha-svg-icon
slot="icon"
.path=${mdiDrag}
data-handle
></ha-svg-icon>
<ha-svg-icon slot="icon" .path=${mdiDrag}></ha-svg-icon>
${label}
</ha-input-chip>
`;

View File

@@ -276,6 +276,8 @@ export class HaAreaPicker extends LitElement {
icon: null,
aliases: [],
labels: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},
@@ -294,6 +296,8 @@ export class HaAreaPicker extends LitElement {
icon: "mdi:plus",
aliases: [],
labels: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},
@@ -378,6 +382,8 @@ export class HaAreaPicker extends LitElement {
picture: null,
labels: [],
aliases: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},
@@ -396,6 +402,8 @@ export class HaAreaPicker extends LitElement {
picture: null,
labels: [],
aliases: [],
temperature_entity_id: null,
humidity_entity_id: null,
created_at: 0,
modified_at: 0,
},

View File

@@ -295,10 +295,12 @@ export class HaAssistChat extends LitElement {
this._addMessage(userMessage);
this.requestUpdate("_audioRecorder");
const hassMessage: AssistMessage = {
let hassMessage = {
who: "hass",
text: "…",
error: false,
};
let currentDeltaRole = "";
// To make sure the answer is placed at the right user text, we add it before we process it
try {
const unsub = await runAssistPipeline(
@@ -328,6 +330,43 @@ export class HaAssistChat extends LitElement {
this._addMessage(hassMessage);
}
if (event.type === "intent-progress") {
const delta = event.data.chat_log_delta;
// new message
if (delta.role) {
// If currentDeltaRole exists, it means we're receiving our
// second or later message. Let's add it to the chat.
if (currentDeltaRole && delta.role && hassMessage.text !== "…") {
// Remove progress indicator of previous message
hassMessage.text = hassMessage.text.substring(
0,
hassMessage.text.length - 1
);
hassMessage = {
who: "hass",
text: "…",
error: false,
};
this._addMessage(hassMessage);
}
currentDeltaRole = delta.role;
}
if (
currentDeltaRole === "assistant" &&
"content" in delta &&
delta.content
) {
hassMessage.text =
hassMessage.text.substring(0, hassMessage.text.length - 1) +
delta.content +
"…";
this.requestUpdate("_conversation");
}
}
if (event.type === "intent-end") {
this._conversationId = event.data.intent_output.conversation_id;
const plain = event.data.intent_output.response.speech?.plain;
@@ -435,28 +474,71 @@ export class HaAssistChat extends LitElement {
this._processing = true;
this._audio?.pause();
this._addMessage({ who: "user", text });
const message: AssistMessage = {
let hassMessage = {
who: "hass",
text: "…",
error: false,
};
let currentDeltaRole = "";
// To make sure the answer is placed at the right user text, we add it before we process it
this._addMessage(message);
this._addMessage(hassMessage);
try {
const unsub = await runAssistPipeline(
this.hass,
(event) => {
if (event.type === "intent-progress") {
const delta = event.data.chat_log_delta;
// new message and previous message has content
if (delta.role) {
// If currentDeltaRole exists, it means we're receiving our
// second or later message. Let's add it to the chat.
if (
currentDeltaRole &&
delta.role === "assistant" &&
hassMessage.text !== "…"
) {
// Remove progress indicator of previous message
hassMessage.text = hassMessage.text.substring(
0,
hassMessage.text.length - 1
);
hassMessage = {
who: "hass",
text: "…",
error: false,
};
this._addMessage(hassMessage);
}
currentDeltaRole = delta.role;
}
if (
currentDeltaRole === "assistant" &&
"content" in delta &&
delta.content
) {
hassMessage.text =
hassMessage.text.substring(0, hassMessage.text.length - 1) +
delta.content +
"…";
this.requestUpdate("_conversation");
}
}
if (event.type === "intent-end") {
this._conversationId = event.data.intent_output.conversation_id;
const plain = event.data.intent_output.response.speech?.plain;
if (plain) {
message.text = plain.speech;
hassMessage.text = plain.speech;
}
this.requestUpdate("_conversation");
unsub();
}
if (event.type === "error") {
message.text = event.data.message;
message.error = true;
hassMessage.text = event.data.message;
hassMessage.error = true;
this.requestUpdate("_conversation");
unsub();
}
@@ -470,8 +552,8 @@ export class HaAssistChat extends LitElement {
}
);
} catch {
message.text = this.hass.localize("ui.dialogs.voice_command.error");
message.error = true;
hassMessage.text = this.hass.localize("ui.dialogs.voice_command.error");
hassMessage.error = true;
this.requestUpdate("_conversation");
} finally {
this._processing = false;
@@ -532,7 +614,7 @@ export class HaAssistChat extends LitElement {
float: var(--float-end);
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--primary-color);
background-color: var(--chat-background-color-user, var(--primary-color));
color: var(--text-primary-color);
direction: var(--direction);
}
@@ -543,7 +625,10 @@ export class HaAssistChat extends LitElement {
margin-inline-start: initial;
float: var(--float-start);
border-bottom-left-radius: 0px;
background-color: var(--secondary-background-color);
background-color: var(
--chat-background-color-hass,
var(--secondary-background-color)
);
color: var(--primary-text-color);
direction: var(--direction);

View File

@@ -329,14 +329,12 @@ export class HaBaseTimeInput extends LitElement {
:host([clearable]) {
position: relative;
}
:host {
display: block;
}
.time-input-wrap-wrap {
display: flex;
}
.time-input-wrap {
display: flex;
flex: var(--time-input-flex, unset);
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
overflow: hidden;
position: relative;
@@ -345,6 +343,7 @@ export class HaBaseTimeInput extends LitElement {
}
ha-textfield {
width: 55px;
flex-grow: 1;
text-align: center;
--mdc-shape-small: 0;
--text-field-appearance: none;

View File

@@ -23,6 +23,9 @@ export class HaButton extends Button {
.slot-container {
overflow: var(--button-slot-container-overflow, visible);
}
:host([destructive]) {
--mdc-theme-primary: var(--error-color);
}
`,
];
}

View File

@@ -9,12 +9,13 @@ import {
endOfMonth,
endOfWeek,
endOfYear,
isThisYear,
startOfDay,
startOfMonth,
startOfWeek,
startOfYear,
isThisYear,
} from "date-fns";
import { fromZonedTime, toZonedTime } from "date-fns-tz";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -22,16 +23,18 @@ import { ifDefined } from "lit/directives/if-defined";
import { calcDate, shiftDateRange } from "../common/datetime/calc_date";
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
import {
formatShortDateTimeWithYear,
formatShortDateTime,
formatShortDateTimeWithYear,
} from "../common/datetime/format_date_time";
import { useAmPm } from "../common/datetime/use_am_pm";
import { fireEvent } from "../common/dom/fire_event";
import { TimeZone } from "../data/translation";
import type { HomeAssistant } from "../types";
import "./date-range-picker";
import "./ha-icon-button";
import "./ha-textarea";
import "./ha-icon-button-next";
import "./ha-icon-button-prev";
import "./ha-textarea";
export type DateRangePickerRanges = Record<string, [Date, Date]>;
@@ -51,7 +54,7 @@ export class HaDateRangePicker extends LitElement {
public autoApply = false;
@property({ attribute: "time-picker", type: Boolean })
public timePicker = true;
public timePicker = false;
@property({ type: Boolean }) public disabled = false;
@@ -197,14 +200,15 @@ export class HaDateRangePicker extends LitElement {
?auto-apply=${this.autoApply}
time-picker=${this.timePicker}
twentyfour-hours=${this._hour24format}
start-date=${this.startDate.toISOString()}
end-date=${this.endDate.toISOString()}
start-date=${this._formatDate(this.startDate)}
end-date=${this._formatDate(this.endDate)}
?ranges=${this.ranges !== false}
opening-direction=${ifDefined(
this.openingDirection || this._calcedOpeningDirection
)}
first-day=${firstWeekdayIndex(this.hass.locale)}
language=${this.hass.locale.language}
@change=${this._handleChange}
>
<div slot="input" class="date-range-inputs" @click=${this._handleClick}>
${!this.minimal
@@ -325,9 +329,31 @@ export class HaDateRangePicker extends LitElement {
}
private _applyDateRange() {
if (this.hass.locale.time_zone === TimeZone.server) {
const dateRangePicker = this._dateRangePicker;
const startDate = fromZonedTime(
dateRangePicker.start,
this.hass.config.time_zone
);
const endDate = fromZonedTime(
dateRangePicker.end,
this.hass.config.time_zone
);
dateRangePicker.clickRange([startDate, endDate]);
}
this._dateRangePicker.clickedApply();
}
private _formatDate(date: Date): string {
if (this.hass.locale.time_zone === TimeZone.server) {
return toZonedTime(date, this.hass.config.time_zone).toISOString();
}
return date.toISOString();
}
private get _dateRangePicker() {
const dateRangePicker = this.shadowRoot!.querySelector(
"date-range-picker"
@@ -358,6 +384,16 @@ export class HaDateRangePicker extends LitElement {
}
}
private _handleChange(ev: CustomEvent) {
ev.stopPropagation();
const startDate = ev.detail.startDate;
const endDate = ev.detail.endDate;
fireEvent(this, "value-changed", {
value: { startDate, endDate },
});
}
static styles = css`
ha-icon-button {

View File

@@ -11,6 +11,7 @@ import "./ha-icon-button";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import { ensureArray } from "../common/array/ensure-array";
import { bytesToString } from "../util/bytes-to-string";
import type { LocalizeFunc } from "../common/translations/localize";
declare global {
interface HASSDomEvents {
@@ -23,6 +24,8 @@ declare global {
export class HaFileUpload extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public localize?: LocalizeFunc;
@property() public accept!: string;
@property() public icon?: string;
@@ -31,6 +34,10 @@ export class HaFileUpload extends LitElement {
@property() public secondary?: string;
@property({ attribute: "uploading-label" }) public uploadingLabel?: string;
@property({ attribute: "delete-label" }) public deleteLabel?: string;
@property() public supports?: string;
@property({ type: Object }) public value?: File | File[] | FileList | string;
@@ -73,23 +80,22 @@ export class HaFileUpload extends LitElement {
}
public render(): TemplateResult {
const localize = this.localize || this.hass!.localize;
return html`
${this.uploading
? html`<div class="container">
<div class="uploading">
<span class="header"
>${this.value
? this.hass?.localize(
"ui.components.file-upload.uploading_name",
{ name: this._name }
)
: this.hass?.localize(
"ui.components.file-upload.uploading"
)}</span
>${this.uploadingLabel || this.value
? localize("ui.components.file-upload.uploading_name", {
name: this._name,
})
: localize("ui.components.file-upload.uploading")}</span
>
${this.progress
? html`<div class="progress">
${this.progress}${blankBeforePercent(this.hass!.locale)}%
${this.progress}${this.hass &&
blankBeforePercent(this.hass!.locale)}%
</div>`
: nothing}
</div>
@@ -116,14 +122,11 @@ export class HaFileUpload extends LitElement {
.path=${this.icon || mdiFileUpload}
></ha-svg-icon>
<ha-button unelevated @click=${this._openFilePicker}>
${this.label ||
this.hass?.localize("ui.components.file-upload.label")}
${this.label || localize("ui.components.file-upload.label")}
</ha-button>
<span class="secondary"
>${this.secondary ||
this.hass?.localize(
"ui.components.file-upload.secondary"
)}</span
localize("ui.components.file-upload.secondary")}</span
>
<span class="supports">${this.supports}</span>`
: typeof this.value === "string"
@@ -136,8 +139,7 @@ export class HaFileUpload extends LitElement {
</div>
<ha-icon-button
@click=${this._clearValue}
.label=${this.hass?.localize("ui.common.delete") ||
"Delete"}
.label=${this.deleteLabel || localize("ui.common.delete")}
.path=${mdiDelete}
></ha-icon-button>
</div>`
@@ -155,8 +157,8 @@ export class HaFileUpload extends LitElement {
</div>
<ha-icon-button
@click=${this._clearValue}
.label=${this.hass?.localize("ui.common.delete") ||
"Delete"}
.label=${this.deleteLabel ||
localize("ui.common.delete")}
.path=${mdiDelete}
></ha-icon-button>
</div>`
@@ -238,6 +240,10 @@ export class HaFileUpload extends LitElement {
border-radius: var(--mdc-shape-small, 4px);
height: 100%;
}
.row {
display: flex;
align-items: center;
}
label.container {
border: dashed 1px
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));

View File

@@ -79,6 +79,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
.disabled=${this.disabled}
@opening=${this._handleOpen}
@closing=${this._handleClose}
positioning="fixed"
>
<ha-textfield
slot="trigger"

View File

@@ -17,6 +17,7 @@ export class HaMdListItem extends MdListItem {
}
md-item {
overflow: var(--md-item-overflow, hidden);
align-items: var(--md-item-align-items, center);
}
`,
];

View File

@@ -1,6 +1,6 @@
import { mdiDeleteOutline, mdiPlus } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { haStyle } from "../resources/styles";
@@ -8,6 +8,7 @@ import type { HomeAssistant } from "../types";
import "./ha-button";
import "./ha-icon-button";
import "./ha-textfield";
import "./ha-input-helper-text";
import type { HaTextField } from "./ha-textfield";
@customElement("ha-multi-textfield")
@@ -20,6 +21,8 @@ class HaMultiTextField extends LitElement {
@property() public label?: string;
@property({ attribute: false }) public helper?: string;
@property({ attribute: false }) public inputType?: string;
@property({ attribute: false }) public inputSuffix?: string;
@@ -69,12 +72,21 @@ class HaMultiTextField extends LitElement {
</div>
`;
})}
<div class="layout horizontal center-center">
<div class="layout horizontal">
<ha-button @click=${this._addItem} .disabled=${this.disabled}>
${this.addLabel ?? this.hass?.localize("ui.common.add") ?? "Add"}
${this.addLabel ??
(this.label
? this.hass?.localize("ui.components.multi-textfield.add_item", {
item: this.label,
})
: this.hass?.localize("ui.common.add")) ??
"Add"}
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-button>
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: nothing}
`;
}

View File

@@ -64,9 +64,13 @@ export class HaNetwork extends LitElement {
>
</ha-checkbox>
</span>
<span slot="heading" data-for="auto_configure"> Auto Configure </span>
<span slot="heading" data-for="auto_configure">
${this.hass.localize(
"ui.panel.config.network.adapter.auto_configure"
)}
</span>
<span slot="description" data-for="auto_configure">
Detected:
${this.hass.localize("ui.panel.config.network.adapter.detected")}:
${format_auto_detected_interfaces(this.networkConfig.adapters)}
</span>
</ha-settings-row>
@@ -85,18 +89,21 @@ export class HaNetwork extends LitElement {
</ha-checkbox>
</span>
<span slot="heading">
Adapter: ${adapter.name}
${this.hass.localize(
"ui.panel.config.network.adapter.adapter"
)}:
${adapter.name}
${adapter.default
? html`<ha-svg-icon .path=${mdiStar}></ha-svg-icon>
(Default)`
: ""}
(${this.hass.localize("ui.common.default")})`
: nothing}
</span>
<span slot="description">
${format_addresses([...adapter.ipv4, ...adapter.ipv6])}
</span>
</ha-settings-row>`
)
: ""}
: nothing}
`;
}

View File

@@ -8,7 +8,7 @@ import { customElement, property, query, state } from "lit/decorators";
// and "qr-scanner" defaults to a suboptimal implementation if it is not available.
// The following import makes a better implementation available that is based on a
// WebAssembly port of ZXing:
import { setZXingModuleOverrides } from "barcode-detector";
import { prepareZXingModule } from "barcode-detector";
import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
@@ -21,12 +21,14 @@ import "./ha-list-item";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
setZXingModuleOverrides({
locateFile: (path: string, prefix: string) => {
if (path.endsWith(".wasm")) {
return "/static/js/zxing_reader.wasm";
}
return prefix + path;
prepareZXingModule({
overrides: {
locateFile: (path: string, prefix: string) => {
if (path.endsWith(".wasm")) {
return "/static/js/zxing_reader.wasm";
}
return prefix + path;
},
},
});

View File

@@ -156,6 +156,7 @@ export class HaSelectSelector extends LitElement {
no-style
.disabled=${!this.selector.select.reorder}
@item-moved=${this._itemMoved}
handle-selector="button.primary.action"
>
<ha-chip-set>
${repeat(
@@ -177,7 +178,6 @@ export class HaSelectSelector extends LitElement {
<ha-svg-icon
slot="icon"
.path=${mdiDrag}
data-handle
></ha-svg-icon>
`
: nothing}

View File

@@ -50,6 +50,7 @@ export class HaTextSelector extends LitElement {
.inputType=${this.selector.text?.type}
.inputSuffix=${this.selector.text?.suffix}
.inputPrefix=${this.selector.text?.prefix}
.helper=${this.helper}
.autocomplete=${this.selector.text?.autocomplete}
@value-changed=${this._handleChange}
>

View File

@@ -1115,6 +1115,8 @@ export class HaMediaPlayerBrowse extends LitElement {
.child .play:not(.can_expand) {
--mdc-icon-button-size: 70px;
--mdc-icon-size: 48px;
background-color: var(--primary-color);
color: var(--text-primary-color);
}
ha-card:hover .image {
@@ -1126,10 +1128,6 @@ export class HaMediaPlayerBrowse extends LitElement {
opacity: 1;
}
ha-card:hover .play:not(.can_expand) {
color: var(--primary-text-color);
}
ha-card:hover .play.can_expand {
bottom: 8px;
}
@@ -1144,10 +1142,6 @@ export class HaMediaPlayerBrowse extends LitElement {
opacity 0.1s ease-out;
}
.child .play:hover {
color: var(--primary-color);
}
.child .title {
font-size: 16px;
padding-top: 16px;
@@ -1331,11 +1325,6 @@ export class HaMediaPlayerBrowse extends LitElement {
ha-browse-media-tts {
direction: var(--direction);
}
ha-card:hover .play:not(.can_expand) {
background-color: var(--primary-color);
color: var(--text-primary-color);
}
`,
];
}

View File

@@ -1,4 +1,4 @@
import { css, html, LitElement, svg } from "lit";
import { css, html, LitElement, nothing, svg } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { BRANCH_HEIGHT, SPACING } from "./hat-graph-const";
@@ -41,8 +41,8 @@ export class HatGraphBranch extends LitElement {
branches.push({
x: width / 2 + total_width,
height,
start: c.hasAttribute("graphStart"),
end: c.hasAttribute("graphEnd"),
start: c.hasAttribute("graph-start"),
end: c.hasAttribute("graph-end"),
track: c.hasAttribute("track"),
});
total_width += width;
@@ -65,11 +65,8 @@ export class HatGraphBranch extends LitElement {
return html`
<slot name="head"></slot>
${!this.start
? svg`
<svg
id="top"
width="${this._totalWidth}"
>
? html`
<svg id="top" width=${this._totalWidth}>
${this._branches.map((branch) =>
branch.start
? ""
@@ -86,7 +83,7 @@ export class HatGraphBranch extends LitElement {
)}
</svg>
`
: ""}
: nothing}
<div id="branches">
<svg id="lines" width=${this._totalWidth} height=${this._maxHeight}>
${this._branches.map((branch) => {
@@ -107,11 +104,8 @@ export class HatGraphBranch extends LitElement {
</div>
${!this.short
? svg`
<svg
id="bottom"
width="${this._totalWidth}"
>
? html`
<svg id="bottom" width=${this._totalWidth}>
${this._branches.map((branch) => {
if (branch.end) return "";
return svg`
@@ -128,7 +122,7 @@ export class HatGraphBranch extends LitElement {
})}
</svg>
`
: ""}
: nothing}
`;
}

View File

@@ -7,13 +7,15 @@ import type { RegistryEntry } from "./registry";
export { subscribeAreaRegistry } from "./ws-area_registry";
export interface AreaRegistryEntry extends RegistryEntry {
aliases: string[];
area_id: string;
floor_id: string | null;
name: string;
picture: string | null;
humidity_entity_id: string | null;
icon: string | null;
labels: string[];
aliases: string[];
name: string;
picture: string | null;
temperature_entity_id: string | null;
}
export type AreaEntityLookup = Record<string, EntityRegistryEntry[]>;
@@ -21,12 +23,14 @@ export type AreaEntityLookup = Record<string, EntityRegistryEntry[]>;
export type AreaDeviceLookup = Record<string, DeviceRegistryEntry[]>;
export interface AreaRegistryEntryMutableParams {
name: string;
floor_id?: string | null;
picture?: string | null;
icon?: string | null;
aliases?: string[];
floor_id?: string | null;
humidity_entity_id?: string | null;
icon?: string | null;
labels?: string[];
name: string;
picture?: string | null;
temperature_entity_id?: string | null;
}
export const createAreaRegistryEntry = (

View File

@@ -108,6 +108,34 @@ interface PipelineIntentStartEvent extends PipelineEventBase {
intent_input: string;
};
}
interface ConversationChatLogAssistantDelta {
role: "assistant";
content: string;
tool_calls: {
id: string;
tool_name: string;
tool_args: Record<string, unknown>;
}[];
}
interface ConversationChatLogToolResultDelta {
role: "tool_result";
agent_id: string;
tool_call_id: string;
tool_name: string;
tool_result: unknown;
}
interface PipelineIntentProgressEvent extends PipelineEventBase {
type: "intent-progress";
data: {
chat_log_delta:
| Partial<ConversationChatLogAssistantDelta>
// These always come in 1 chunk
| ConversationChatLogToolResultDelta;
};
}
interface PipelineIntentEndEvent extends PipelineEventBase {
type: "intent-end";
data: {
@@ -141,6 +169,7 @@ export type PipelineRunEvent =
| PipelineSTTStartEvent
| PipelineSTTEndEvent
| PipelineIntentStartEvent
| PipelineIntentProgressEvent
| PipelineIntentEndEvent
| PipelineTTSStartEvent
| PipelineTTSEndEvent;

View File

@@ -1,3 +1,4 @@
import { memoize } from "@fullcalendar/core/internal";
import { setHours, setMinutes } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
@@ -11,22 +12,35 @@ import type { HomeAssistant } from "../types";
import { fileDownload } from "../util/file_download";
import { domainToName } from "./integration";
import type { FrontendLocaleData } from "./translation";
import checkValidDate from "../common/datetime/check_valid_date";
import { handleFetchPromise } from "../util/hass-call-api";
export const enum BackupScheduleState {
export const enum BackupScheduleRecurrence {
NEVER = "never",
DAILY = "daily",
MONDAY = "mon",
TUESDAY = "tue",
WEDNESDAY = "wed",
THURSDAY = "thu",
FRIDAY = "fri",
SATURDAY = "sat",
SUNDAY = "sun",
CUSTOM_DAYS = "custom_days",
}
export type BackupDay = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun";
export const BACKUP_DAYS: BackupDay[] = [
"mon",
"tue",
"wed",
"thu",
"fri",
"sat",
"sun",
];
export const sortWeekdays = (weekdays) =>
weekdays.sort((a, b) => BACKUP_DAYS.indexOf(a) - BACKUP_DAYS.indexOf(b));
export interface BackupConfig {
last_attempted_automatic_backup: string | null;
last_completed_automatic_backup: string | null;
next_automatic_backup: string | null;
next_automatic_backup_additional?: boolean;
create_backup: {
agent_ids: string[];
include_addons: string[] | null;
@@ -41,8 +55,11 @@ export interface BackupConfig {
days?: number | null;
};
schedule: {
state: BackupScheduleState;
recurrence: BackupScheduleRecurrence;
time?: string | null;
days: BackupDay[];
};
agents: BackupAgentsConfig;
}
export interface BackupMutableConfig {
@@ -59,21 +76,39 @@ export interface BackupMutableConfig {
copies?: number | null;
days?: number | null;
};
schedule?: BackupScheduleState;
schedule?: {
recurrence: BackupScheduleRecurrence;
time?: string | null;
days?: BackupDay[] | null;
};
agents?: BackupAgentsConfig;
}
export type BackupAgentsConfig = Record<string, BackupAgentConfig>;
export interface BackupAgentConfig {
protected: boolean;
}
export interface BackupAgent {
agent_id: string;
name: string;
}
export interface BackupContentAgent {
size: number;
protected: boolean;
}
export interface BackupContent {
backup_id: string;
date: string;
name: string;
protected: boolean;
size: number;
agent_ids?: string[];
agents: Record<string, BackupContentAgent>;
failed_agent_ids?: string[];
extra_metadata?: {
"supervisor.addon_update"?: string;
};
with_automatic_settings: boolean;
}
@@ -135,8 +170,12 @@ export const updateBackupConfig = (
config: BackupMutableConfig
) => hass.callWS({ type: "backup/config/update", ...config });
export const getBackupDownloadUrl = (id: string, agentId: string) =>
`/api/backup/download/${id}?agent_id=${agentId}`;
export const getBackupDownloadUrl = (
id: string,
agentId: string,
password?: string | null
) =>
`/api/backup/download/${id}?agent_id=${agentId}${password ? `&password=${password}` : ""}`;
export const fetchBackupInfo = (hass: HomeAssistant): Promise<BackupInfo> =>
hass.callWS({
@@ -193,27 +232,23 @@ export const restoreBackup = (
export const uploadBackup = async (
hass: HomeAssistant,
file: File,
agent_ids: string[]
): Promise<void> => {
agentIds: string[]
): Promise<{ backup_id: string }> => {
const fd = new FormData();
fd.append("file", file);
const params = agent_ids.reduce((acc, agent_id) => {
acc.append("agent_id", agent_id);
return acc;
}, new URLSearchParams());
const params = new URLSearchParams();
const resp = await hass.fetchWithAuth(
`/api/backup/upload?${params.toString()}`,
{
agentIds.forEach((agentId) => {
params.append("agent_id", agentId);
});
return handleFetchPromise(
hass.fetchWithAuth(`/api/backup/upload?${params.toString()}`, {
method: "POST",
body: fd,
}
})
);
if (!resp.ok) {
throw new Error(`${resp.status} ${resp.statusText}`);
}
};
export const getPreferredAgentForDownload = (agents: string[]) => {
@@ -229,6 +264,19 @@ export const getPreferredAgentForDownload = (agents: string[]) => {
return agents[0];
};
export const canDecryptBackupOnDownload = (
hass: HomeAssistant,
backup_id: string,
agent_id: string,
password: string
) =>
hass.callWS({
type: "backup/can_decrypt_on_download",
backup_id,
agent_id,
password,
});
export const CORE_LOCAL_AGENT = "backup.local";
export const HASSIO_LOCAL_AGENT = "hassio.local";
export const CLOUD_AGENT = "cloud.cloud";
@@ -244,13 +292,18 @@ export const isNetworkMountAgent = (agentId: string) => {
export const computeBackupAgentName = (
localize: LocalizeFunc,
agentId: string,
agentIds?: string[]
agents: BackupAgent[]
) => {
if (isLocalAgent(agentId)) {
return localize("ui.panel.config.backup.agents.local_agent");
}
const [domain, name] = agentId.split(".");
const agent = agents.find((a) => a.agent_id === agentId);
const domain = agentId.split(".")[0];
const name = agent ? agent.name : agentId.split(".")[1];
// If it's a network mount agent, only show the name
if (isNetworkMountAgent(agentId)) {
return name;
}
@@ -258,13 +311,38 @@ export const computeBackupAgentName = (
const domainName = domainToName(localize, domain);
// If there are multiple agents for a domain, show the name
const showName = agentIds
? agentIds.filter((a) => a.split(".")[0] === domain).length > 1
: true;
const showName =
agents.filter((a) => a.agent_id.split(".")[0] === domain).length > 1;
return showName ? `${domainName}: ${name}` : domainName;
};
export const computeBackupSize = (backup: BackupContent) =>
Math.max(...Object.values(backup.agents).map((agent) => agent.size));
export type BackupType = "automatic" | "manual" | "addon_update";
const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "manual", "addon_update"];
export const getBackupTypes = memoize((isHassio: boolean) =>
isHassio
? BACKUP_TYPE_ORDER
: BACKUP_TYPE_ORDER.filter((type) => type !== "addon_update")
);
export const computeBackupType = (
backup: BackupContent,
isHassio: boolean
): BackupType => {
if (backup.with_automatic_settings) {
return "automatic";
}
if (isHassio && backup.extra_metadata?.["supervisor.addon_update"] != null) {
return "addon_update";
}
return "manual";
};
export const compareAgents = (a: string, b: string) => {
const isLocalA = isLocalAgent(a);
const isLocalB = isLocalAgent(b);
@@ -337,9 +415,44 @@ export const downloadEmergencyKit = (
geneateEmergencyKitFileName(hass, appendFileName)
);
export const DEFAULT_OPTIMIZED_BACKUP_START_TIME = setMinutes(
setHours(new Date(), 4),
45
);
export const DEFAULT_OPTIMIZED_BACKUP_END_TIME = setMinutes(
setHours(new Date(), 5),
45
);
export const getFormattedBackupTime = memoizeOne(
(locale: FrontendLocaleData, config: HassConfig) => {
const date = setMinutes(setHours(new Date(), 4), 45);
return formatTime(date, locale, config);
(
locale: FrontendLocaleData,
config: HassConfig,
backupTime?: Date | string | null
) => {
if (checkValidDate(backupTime as Date)) {
return formatTime(backupTime as Date, locale, config);
}
if (typeof backupTime === "string" && backupTime) {
const splitted = backupTime.split(":");
const date = setMinutes(
setHours(new Date(), parseInt(splitted[0])),
parseInt(splitted[1])
);
return formatTime(date, locale, config);
}
return `${formatTime(DEFAULT_OPTIMIZED_BACKUP_START_TIME, locale, config)} - ${formatTime(DEFAULT_OPTIMIZED_BACKUP_END_TIME, locale, config)}`;
}
);
export const SUPPORTED_UPLOAD_FORMAT = "application/x-tar";
export interface BackupUploadFileFormData {
file?: File;
}
export const INITIAL_UPLOAD_FORM_DATA: BackupUploadFileFormData = {
file: undefined,
};

View File

@@ -0,0 +1,66 @@
import { handleFetchPromise } from "../util/hass-call-api";
import type { BackupContentExtended } from "./backup";
import type {
BackupManagerState,
RestoreBackupStage,
RestoreBackupState,
} from "./backup_manager";
export interface BackupOnboardingInfo {
state: BackupManagerState;
last_non_idle_event?: {
manager_state: BackupManagerState;
stage: RestoreBackupStage | null;
state: RestoreBackupState;
reason: string | null;
} | null;
}
export interface BackupOnboardingConfig extends BackupOnboardingInfo {
backups: BackupContentExtended[];
}
export const fetchBackupOnboardingInfo = async () =>
handleFetchPromise<BackupOnboardingConfig>(
fetch("/api/onboarding/backup/info")
);
export interface RestoreOnboardingBackupParams {
backup_id: string;
agent_id: string;
password?: string;
restore_addons?: string[];
restore_database?: boolean;
restore_folders?: string[];
}
export const restoreOnboardingBackup = async (
params: RestoreOnboardingBackupParams
) =>
handleFetchPromise(
fetch("/api/onboarding/backup/restore", {
method: "POST",
body: JSON.stringify(params),
})
);
export const uploadOnboardingBackup = async (
file: File,
agentIds: string[]
): Promise<{ backup_id: string }> => {
const fd = new FormData();
fd.append("file", file);
const params = new URLSearchParams();
agentIds.forEach((agentId) => {
params.append("agent_id", agentId);
});
return handleFetchPromise(
fetch(`/api/onboarding/backup/upload?${params.toString()}`, {
method: "POST",
body: fd,
})
);
};

167
src/data/bluetooth.ts Normal file
View File

@@ -0,0 +1,167 @@
import {
createCollection,
type Connection,
type UnsubscribeFunc,
} from "home-assistant-js-websocket";
import type { Store } from "home-assistant-js-websocket/dist/store";
import type { DataTableRowData } from "../components/data-table/ha-data-table";
export interface BluetoothDeviceData extends DataTableRowData {
address: string;
connectable: boolean;
manufacturer_data: Record<number, string>;
name: string;
rssi: number;
service_data: Record<string, string>;
service_uuids: string[];
source: string;
time: number;
tx_power: number;
}
export interface BluetoothScannerDetails {
source: string;
connectable: boolean;
name: string;
adapter: string;
}
export type BluetoothScannersDetails = Record<string, BluetoothScannerDetails>;
interface BluetoothRemoveDeviceData {
address: string;
}
interface BluetoothAdvertisementSubscriptionMessage {
add?: BluetoothDeviceData[];
change?: BluetoothDeviceData[];
remove?: BluetoothRemoveDeviceData[];
}
interface BluetoothScannersDetailsSubscriptionMessage {
add?: BluetoothScannerDetails[];
remove?: BluetoothScannerDetails[];
}
export interface BluetoothAllocationsData {
source: string;
slots: number;
free: number;
allocated: string[];
}
export const subscribeBluetoothScannersDetailsUpdates = (
conn: Connection,
store: Store<BluetoothScannersDetails>
): Promise<UnsubscribeFunc> =>
conn.subscribeMessage<BluetoothScannersDetailsSubscriptionMessage>(
(event) => {
const data = { ...(store.state || {}) };
if (event.add) {
for (const device_data of event.add) {
data[device_data.source] = device_data;
}
}
if (event.remove) {
for (const device_data of event.remove) {
delete data[device_data.source];
}
}
store.setState(data, true);
},
{
type: `bluetooth/subscribe_scanner_details`,
}
);
export const subscribeBluetoothScannersDetails = (
conn: Connection,
callbackFunction: (bluetoothScannersDetails: BluetoothScannersDetails) => void
) =>
createCollection<BluetoothScannersDetails>(
"_bluetoothScannerDetails",
() => Promise.resolve<BluetoothScannersDetails>({}), // empty hash as initial state
subscribeBluetoothScannersDetailsUpdates,
conn,
callbackFunction
);
const subscribeBluetoothAdvertisementsUpdates = (
conn: Connection,
store: Store<BluetoothDeviceData[]>
): Promise<UnsubscribeFunc> =>
conn.subscribeMessage<BluetoothAdvertisementSubscriptionMessage>(
(event) => {
const data = [...(store.state || [])];
if (event.add) {
for (const device_data of event.add) {
const index = data.findIndex(
(d) => d.address === device_data.address
);
if (index === -1) {
data.push(device_data);
} else {
data[index] = device_data;
}
}
}
if (event.change) {
for (const device_data of event.change) {
const index = data.findIndex(
(d) => d.address === device_data.address
);
if (index !== -1) {
data[index] = device_data;
}
}
}
if (event.remove) {
for (const device_data of event.remove) {
const index = data.findIndex(
(d) => d.address === device_data.address
);
if (index !== -1) {
data.splice(index, 1);
}
}
}
store.setState(data, true);
},
{
type: `bluetooth/subscribe_advertisements`,
}
);
export const subscribeBluetoothAdvertisements = (
conn: Connection,
callbackFunction: (bluetoothDeviceData: BluetoothDeviceData[]) => void
) =>
createCollection<BluetoothDeviceData[]>(
"_bluetoothDeviceRows",
() => Promise.resolve<BluetoothDeviceData[]>([]), // empty array as initial state
subscribeBluetoothAdvertisementsUpdates,
conn,
callbackFunction
);
export const subscribeBluetoothConnectionAllocations = (
conn: Connection,
callbackFunction: (
bluetoothAllocationsData: BluetoothAllocationsData[]
) => void,
configEntryId?: string
): Promise<() => Promise<void>> => {
const params: { type: string; config_entry_id?: string } = {
type: "bluetooth/subscribe_connection_allocations",
};
if (configEntryId) {
params.config_entry_id = configEntryId;
}
return conn.subscribeMessage<BluetoothAllocationsData[]>(
(bluetoothAllocationsData) => callbackFunction(bluetoothAllocationsData),
params
);
};

View File

@@ -181,3 +181,6 @@ export const updateCloudGoogleEntityConfig = (
export const cloudSyncGoogleAssistant = (hass: HomeAssistant) =>
hass.callApi("POST", "cloud/google_actions/sync");
export const fetchSupportPackage = (hass: HomeAssistant) =>
hass.callApi<string>("GET", "cloud/support_package");

View File

@@ -19,6 +19,8 @@ export interface ConfigEntry {
supports_remove_device: boolean;
supports_unload: boolean;
supports_reconfigure: boolean;
supported_subentry_types: Record<string, { supports_reconfigure: boolean }>;
num_subentries: number;
pref_disable_new_entities: boolean;
pref_disable_polling: boolean;
disabled_by: "user" | null;
@@ -27,6 +29,30 @@ export interface ConfigEntry {
error_reason_translation_placeholders: Record<string, string> | null;
}
export interface SubEntry {
subentry_id: string;
subentry_type: string;
title: string;
unique_id: string;
}
export const getSubEntries = (hass: HomeAssistant, entry_id: string) =>
hass.callWS<SubEntry[]>({
type: "config_entries/subentries/list",
entry_id,
});
export const deleteSubEntry = (
hass: HomeAssistant,
entry_id: string,
subentry_id: string
) =>
hass.callWS({
type: "config_entries/subentries/delete",
entry_id,
subentry_id,
});
export type ConfigEntryMutableParams = Partial<
Pick<
ConfigEntry,

View File

@@ -2,7 +2,11 @@ import type { Connection } from "home-assistant-js-websocket";
import type { HaFormSchema } from "../components/ha-form/types";
import type { ConfigEntry } from "./config_entries";
export type FlowType = "config_flow" | "options_flow" | "repair_flow";
export type FlowType =
| "config_flow"
| "config_subentries_flow"
| "options_flow"
| "repair_flow";
export interface DataEntryFlowProgressedEvent {
type: "data_entry_flow_progressed";

View File

@@ -17,6 +17,7 @@ export {
export interface DeviceRegistryEntry extends RegistryEntry {
id: string;
config_entries: string[];
config_entries_subentries: Record<string, (string | null)[]>;
connections: [string, string][];
identifiers: [string, string][];
manufacturer: string | null;

View File

@@ -50,6 +50,7 @@ export interface EntityRegistryEntry extends RegistryEntry {
icon: string | null;
platform: string;
config_entry_id: string | null;
config_subentry_id: string | null;
device_id: string | null;
area_id: string | null;
labels: string[];

View File

@@ -313,21 +313,34 @@ export const installHassioAddon = async (
export const updateHassioAddon = async (
hass: HomeAssistant,
slug: string
slug: string,
backup: boolean
): Promise<void> => {
if (atLeastVersion(hass.config.version, 2025, 2, 0)) {
await hass.callWS({
type: "hassio/update/addon",
addon: slug,
backup: backup,
});
return;
}
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/store/addons/${slug}/update`,
method: "post",
timeout: null,
data: { backup },
});
} else {
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/update`
);
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/update`,
{ backup }
);
};
export const restartHassioAddon = async (

View File

@@ -1,6 +1,5 @@
import { atLeastVersion } from "../../common/config/version";
import type { HomeAssistant } from "../../types";
import { handleFetchPromise } from "../../util/hass-call-api";
import type { HassioResponse } from "./common";
import { hassioApiResultExtractor } from "./common";
@@ -82,34 +81,24 @@ export const fetchHassioBackups = async (
};
export const fetchHassioBackupInfo = async (
hass: HomeAssistant | undefined,
hass: HomeAssistant,
backup: string
): Promise<HassioBackupDetail> => {
if (hass) {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS({
type: "supervisor/api",
endpoint: `/${
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
}/${backup}/info`,
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioBackupDetail>>(
"GET",
`hassio/${
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
}/${backup}/info`
)
);
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS({
type: "supervisor/api",
endpoint: `/${
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
}/${backup}/info`,
method: "get",
});
}
// When called from onboarding we don't have hass
return hassioApiResultExtractor(
await handleFetchPromise(
fetch(`/api/hassio/backups/${backup}/info`, {
method: "GET",
})
await hass.callApi<HassioResponse<HassioBackupDetail>>(
"GET",
`hassio/${
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
}/${backup}/info`
)
);
};
@@ -240,24 +229,15 @@ export const uploadBackup = async (
};
export const restoreBackup = async (
hass: HomeAssistant | undefined,
hass: HomeAssistant,
type: HassioBackupDetail["type"],
backupSlug: string,
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
useSnapshotUrl: boolean
): Promise<void> => {
if (hass) {
await hass.callApi<HassioResponse<{ job_id: string }>>(
"POST",
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
backupDetails
);
} else {
await handleFetchPromise(
fetch(`/api/hassio/backups/${backupSlug}/restore/${type}`, {
method: "POST",
body: JSON.stringify(backupDetails),
})
);
}
await hass.callApi<HassioResponse<{ job_id: string }>>(
"POST",
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
backupDetails
);
};

View File

@@ -5,6 +5,7 @@ import type { HomeAssistant } from "../types";
import { debounce } from "../common/util/debounce";
export const integrationsWithPanel = {
bluetooth: "config/bluetooth",
matter: "config/matter",
mqtt: "config/mqtt",
thread: "config/thread",

View File

@@ -2,6 +2,8 @@ import type { HomeAssistant } from "../types";
export const SENSOR_DEVICE_CLASS_BATTERY = "battery";
export const SENSOR_DEVICE_CLASS_TIMESTAMP = "timestamp";
export const SENSOR_DEVICE_CLASS_TEMPERATURE = "temperature";
export const SENSOR_DEVICE_CLASS_HUMIDITY = "humidity";
export interface SensorDeviceClassUnits {
units: string[];

View File

@@ -0,0 +1,46 @@
import type { HomeAssistant } from "../types";
import type { DataEntryFlowStep } from "./data_entry_flow";
const HEADERS = {
"HA-Frontend-Base": `${location.protocol}//${location.host}`,
};
export const createSubConfigFlow = (
hass: HomeAssistant,
configEntryId: string,
subFlowType: string,
subentry_id?: string
) =>
hass.callApi<DataEntryFlowStep>(
"POST",
"config/config_entries/subentries/flow",
{
handler: [configEntryId, subFlowType],
show_advanced_options: Boolean(hass.userData?.showAdvanced),
subentry_id,
},
HEADERS
);
export const fetchSubConfigFlow = (hass: HomeAssistant, flowId: string) =>
hass.callApi<DataEntryFlowStep>(
"GET",
`config/config_entries/subentries/flow/${flowId}`,
undefined,
HEADERS
);
export const handleSubConfigFlowStep = (
hass: HomeAssistant,
flowId: string,
data: Record<string, any>
) =>
hass.callApi<DataEntryFlowStep>(
"POST",
`config/config_entries/subentries/flow/${flowId}`,
data,
HEADERS
);
export const deleteSubConfigFlow = (hass: HomeAssistant, flowId: string) =>
hass.callApi("DELETE", `config/config_entries/subentries/flow/${flowId}`);

View File

@@ -6,15 +6,27 @@ export const restartCore = async (hass: HomeAssistant) => {
await hass.callService("homeassistant", "restart");
};
export const updateCore = async (hass: HomeAssistant) => {
export const updateCore = async (hass: HomeAssistant, backup: boolean) => {
if (atLeastVersion(hass.config.version, 2025, 2, 0)) {
await hass.callWS({
type: "hassio/update/core",
backup: backup,
});
return;
}
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: "/core/update",
method: "post",
timeout: null,
data: { backup },
});
} else {
await hass.callApi<HassioResponse<void>>("POST", "hassio/core/update");
return;
}
await hass.callApi<HassioResponse<void>>("POST", "hassio/core/update", {
backup,
});
};

View File

@@ -63,6 +63,7 @@ export type TranslationCategory =
| "entity_component"
| "exceptions"
| "config"
| "config_subentries"
| "config_panel"
| "options"
| "device_automation"

View File

@@ -13,6 +13,7 @@ import { caseInsensitiveStringCompare } from "../common/string/compare";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../types";
import { showToast } from "../util/toast";
import type { EntitySources } from "./entity_sources";
export enum UpdateEntityFeature {
INSTALL = 1,
@@ -60,6 +61,10 @@ export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
entity_id: entityId,
});
const HOME_ASSISTANT_CORE_TITLE = "Home Assistant Core";
const HOME_ASSISTANT_SUPERVISOR_TITLE = "Home Assistant Supervisor";
const HOME_ASSISTANT_OS_TITLE = "Home Assistant Operating System";
export const filterUpdateEntities = (
entities: HassEntities,
language?: string
@@ -69,22 +74,22 @@ export const filterUpdateEntities = (
(entity) => computeStateDomain(entity) === "update"
) as UpdateEntity[]
).sort((a, b) => {
if (a.attributes.title === "Home Assistant Core") {
if (a.attributes.title === HOME_ASSISTANT_CORE_TITLE) {
return -3;
}
if (b.attributes.title === "Home Assistant Core") {
if (b.attributes.title === HOME_ASSISTANT_CORE_TITLE) {
return 3;
}
if (a.attributes.title === "Home Assistant Operating System") {
if (a.attributes.title === HOME_ASSISTANT_OS_TITLE) {
return -2;
}
if (b.attributes.title === "Home Assistant Operating System") {
if (b.attributes.title === HOME_ASSISTANT_OS_TITLE) {
return 2;
}
if (a.attributes.title === "Home Assistant Supervisor") {
if (a.attributes.title === HOME_ASSISTANT_SUPERVISOR_TITLE) {
return -1;
}
if (b.attributes.title === "Home Assistant Supervisor") {
if (b.attributes.title === HOME_ASSISTANT_SUPERVISOR_TITLE) {
return 1;
}
return caseInsensitiveStringCompare(
@@ -201,3 +206,32 @@ export const computeUpdateStateDisplay = (
return hass.formatEntityState(stateObj);
};
type UpdateType = "addon" | "home_assistant" | "generic";
export const getUpdateType = (
stateObj: UpdateEntity,
entitySources: EntitySources
): UpdateType => {
const entity_id = stateObj.entity_id;
const domain = entitySources[entity_id]?.domain;
if (domain !== "hassio") {
return "generic";
}
const title = stateObj.attributes.title || "";
if (title === HOME_ASSISTANT_CORE_TITLE) {
return "home_assistant";
}
if (
![
HOME_ASSISTANT_CORE_TITLE,
HOME_ASSISTANT_SUPERVISOR_TITLE,
HOME_ASSISTANT_OS_TITLE,
].includes(title)
) {
return "addon";
}
return "generic";
};

View File

@@ -1,5 +1,11 @@
import type { Connection } from "home-assistant-js-websocket";
import { createCollection } from "home-assistant-js-websocket";
import type { HomeAssistant, ThemeSettings } from "../types";
import {
fetchFrontendUserData,
saveFrontendUserData,
subscribeFrontendUserData,
} from "./frontend";
export interface ThemeVars {
// Incomplete
@@ -50,3 +56,16 @@ export const subscribeThemes = (
conn,
onChange
);
export const SELECTED_THEME_KEY = "selectedTheme";
export const saveSelectedTheme = (hass: HomeAssistant, data?: ThemeSettings) =>
saveFrontendUserData(hass.connection, SELECTED_THEME_KEY, data);
export const subscribeSelectedTheme = (
hass: HomeAssistant,
callback: (selectedTheme?: ThemeSettings | null) => void
) => subscribeFrontendUserData(hass.connection, SELECTED_THEME_KEY, callback);
export const fetchSelectedTheme = (hass: HomeAssistant) =>
fetchFrontendUserData(hass.connection, SELECTED_THEME_KEY);

View File

@@ -282,6 +282,8 @@ class DataEntryFlowDialog extends LitElement {
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.navigateToResult=${this._params
.navigateToResult}
></step-flow-create-entry>
`}
`}
@@ -312,32 +314,31 @@ class DataEntryFlowDialog extends LitElement {
private async _processStep(
step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep>
): Promise<void> {
if (step instanceof Promise) {
this._loading = "loading_step";
try {
this._step = await step;
} catch (err: any) {
this.closeDialog();
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: err?.body?.message,
});
return;
} finally {
this._loading = undefined;
}
return;
}
if (step === undefined) {
this.closeDialog();
return;
}
this._loading = "loading_step";
let _step: DataEntryFlowStep;
try {
_step = await step;
} catch (err: any) {
this.closeDialog();
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: err?.body?.message,
});
return;
} finally {
this._loading = undefined;
}
this._step = undefined;
await this.updateComplete;
this._step = step;
this._step = _step;
}
private async _subscribeDataEntryFlowProgressed() {

View File

@@ -77,7 +77,7 @@ export class FlowPreviewGeneric extends LitElement {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType === "repair_flow") {
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
return;
}
try {

View File

@@ -147,7 +147,7 @@ class FlowPreviewTemplate extends LitElement {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType === "repair_flow") {
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
return;
}
try {

View File

@@ -16,7 +16,9 @@ export const loadConfigFlowDialog = loadDataEntryFlowDialog;
export const showConfigFlowDialog = (
element: HTMLElement,
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig">
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig"> & {
entryId?: string;
}
): void =>
showFlowDialog(element, dialogParams, {
flowType: "config_flow",

View File

@@ -148,8 +148,8 @@ export interface DataEntryFlowDialogParams {
}) => void;
flowConfig: FlowConfig;
showAdvanced?: boolean;
entryId?: string;
dialogParentElement?: HTMLElement;
navigateToResult?: boolean;
}
export const loadDataEntryFlowDialog = () => import("./dialog-data-entry-flow");

View File

@@ -0,0 +1,275 @@
import { html } from "lit";
import type { ConfigEntry } from "../../data/config_entries";
import { domainToName } from "../../data/integration";
import {
createSubConfigFlow,
deleteSubConfigFlow,
fetchSubConfigFlow,
handleSubConfigFlowStep,
} from "../../data/sub_config_flow";
import type { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
import {
loadDataEntryFlowDialog,
showFlowDialog,
} from "./show-dialog-data-entry-flow";
export const loadSubConfigFlowDialog = loadDataEntryFlowDialog;
export const showSubConfigFlowDialog = (
element: HTMLElement,
configEntry: ConfigEntry,
flowType: string,
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig"> & {
subEntryId?: string;
}
): void =>
showFlowDialog(element, dialogParams, {
flowType: "config_subentries_flow",
showDevices: true,
createFlow: async (hass, handler) => {
const [step] = await Promise.all([
createSubConfigFlow(hass, handler, flowType, dialogParams.subEntryId),
hass.loadFragmentTranslation("config"),
hass.loadBackendTranslation("config_subentries", configEntry.domain),
hass.loadBackendTranslation("selector", configEntry.domain),
// Used as fallback if no header defined for step
hass.loadBackendTranslation("title", configEntry.domain),
]);
return step;
},
fetchFlow: async (hass, flowId) => {
const step = await fetchSubConfigFlow(hass, flowId);
await hass.loadFragmentTranslation("config");
await hass.loadBackendTranslation(
"config_subentries",
configEntry.domain
);
await hass.loadBackendTranslation("selector", configEntry.domain);
return step;
},
handleFlowStep: handleSubConfigFlowStep,
deleteFlow: deleteSubConfigFlow,
renderAbortDescription(hass, step) {
const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.abort.${step.reason}`,
step.description_placeholders
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
`
: step.reason;
},
renderShowFormStepHeader(hass, step) {
return (
hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`,
step.description_placeholders
) || hass.localize(`component.${configEntry.domain}.title`)
);
},
renderShowFormStepDescription(hass, step) {
const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
step.description_placeholders
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
`
: "";
},
renderShowFormStepFieldLabel(hass, step, field, options) {
if (field.type === "expandable") {
return hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.name`
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
return (
hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${prefix}data.${field.name}`
) || field.name
);
},
renderShowFormStepFieldHelper(hass, step, field, options) {
if (field.type === "expandable") {
return hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.description`
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${prefix}data_description.${field.name}`,
step.description_placeholders
);
return description
? html`<ha-markdown breaks .content=${description}></ha-markdown>`
: "";
},
renderShowFormStepFieldError(hass, step, error) {
return (
hass.localize(
`component.${step.translation_domain || step.translation_domain || configEntry.domain}.config_subentries.${flowType}.error.${error}`,
step.description_placeholders
) || error
);
},
renderShowFormStepFieldLocalizeValue(hass, _step, key) {
return hass.localize(`component.${configEntry.domain}.selector.${key}`);
},
renderShowFormStepSubmitButton(hass, step) {
return (
hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.submit`
) ||
hass.localize(
`ui.panel.config.integrations.config_flow.${
step.last_step === false ? "next" : "submit"
}`
)
);
},
renderExternalStepHeader(hass, step) {
return (
hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`
) ||
hass.localize(
"ui.panel.config.integrations.config_flow.external_step.open_site"
)
);
},
renderExternalStepDescription(hass, step) {
const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
step.description_placeholders
);
return html`
<p>
${hass.localize(
"ui.panel.config.integrations.config_flow.external_step.description"
)}
</p>
${description
? html`
<ha-markdown
allowsvg
breaks
.content=${description}
></ha-markdown>
`
: ""}
`;
},
renderCreateEntryDescription(hass, step) {
const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.create_entry.${
step.description || "default"
}`,
step.description_placeholders
);
return html`
${description
? html`
<ha-markdown
allowsvg
breaks
.content=${description}
></ha-markdown>
`
: ""}
<p>
${hass.localize(
"ui.panel.config.integrations.config_flow.created_config",
{ name: step.title }
)}
</p>
`;
},
renderShowFormProgressHeader(hass, step) {
return (
hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`
) || hass.localize(`component.${configEntry.domain}.title`)
);
},
renderShowFormProgressDescription(hass, step) {
const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.progress.${step.progress_action}`,
step.description_placeholders
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
`
: "";
},
renderMenuHeader(hass, step) {
return (
hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`,
step.description_placeholders
) || hass.localize(`component.${configEntry.domain}.title`)
);
},
renderMenuDescription(hass, step) {
const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
step.description_placeholders
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
`
: "";
},
renderMenuOption(hass, step, option) {
return hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.menu_options.${option}`,
step.description_placeholders
);
},
renderLoadingDescription(hass, reason, handler, step) {
if (reason !== "loading_flow" && reason !== "loading_step") {
return "";
}
const domain = step?.handler || handler;
return hass.localize(
`ui.panel.config.integrations.config_flow.loading.${reason}`,
{
integration: domain
? domainToName(hass.localize, domain)
: // when we are continuing a config flow, we only know the ID and not the domain
hass.localize(
"ui.panel.config.integrations.config_flow.loading.fallback_title"
),
}
);
},
});

View File

@@ -60,6 +60,7 @@ class StepFlowAbort extends LitElement {
dialogClosedCallback: this.params.dialogClosedCallback,
startFlowHandler: this.domain,
showAdvanced: this.hass.userData?.showAdvanced,
navigateToResult: this.params.navigateToResult,
});
},
});

View File

@@ -19,6 +19,7 @@ import { showAlertDialog } from "../generic/show-dialog-box";
import { showVoiceAssistantSetupDialog } from "../voice-assistant-setup/show-voice-assistant-setup-dialog";
import type { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
import { navigate } from "../../common/navigate";
@customElement("step-flow-create-entry")
class StepFlowCreateEntry extends LitElement {
@@ -28,6 +29,8 @@ class StepFlowCreateEntry extends LitElement {
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
navigateToResult = false;
private _devices = memoizeOne(
(
showDevices: boolean,
@@ -65,7 +68,8 @@ class StepFlowCreateEntry extends LitElement {
if (
devices.length !== 1 ||
devices[0].primary_config_entry !== this.step.result?.entry_id
devices[0].primary_config_entry !== this.step.result?.entry_id ||
this.step.result.domain === "voip"
) {
return;
}
@@ -151,6 +155,11 @@ class StepFlowCreateEntry extends LitElement {
private _flowDone(): void {
fireEvent(this, "flow-update", { step: undefined });
if (this.step.result && this.navigateToResult) {
navigate(
`/config/integrations/integration/${this.step.result.domain}#config_entry=${this.step.result.entry_id}`
);
}
}
private async _areaPicked(ev: CustomEvent) {

View File

@@ -1,7 +1,6 @@
import { mdiAlertOutline } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-md-dialog";
@@ -117,9 +116,7 @@ class DialogBox extends LitElement {
@click=${this._confirm}
?dialogInitialFocus=${!this._params.prompt &&
!this._params.destructive}
class=${classMap({
destructive: this._params.destructive || false,
})}
?destructive=${this._params.destructive}
>
${this._params.confirmText
? this._params.confirmText
@@ -187,9 +184,6 @@ class DialogBox extends LitElement {
.secondary {
color: var(--secondary-text-color);
}
.destructive {
--mdc-theme-primary: var(--error-color);
}
ha-textfield {
width: 100%;
}

View File

@@ -33,6 +33,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
"switch",
"valve",
"water_heater",
"weather",
];
/** Domains with full height more info dialog */
export const DOMAINS_FULL_HEIGHT_MORE_INFO = ["update"];

View File

@@ -2,6 +2,7 @@ import "@material/mwc-linear-progress/mwc-linear-progress";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { BINARY_STATE_OFF } from "../../../common/const";
import { relativeTime } from "../../../common/datetime/relative_time";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-alert";
import "../../../components/ha-button";
@@ -10,10 +11,18 @@ import "../../../components/ha-circular-progress";
import "../../../components/ha-faded";
import "../../../components/ha-formfield";
import "../../../components/ha-markdown";
import "../../../components/ha-settings-row";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import type { BackupConfig } from "../../../data/backup";
import { fetchBackupConfig } from "../../../data/backup";
import { isUnavailableState } from "../../../data/entity";
import type { EntitySources } from "../../../data/entity_sources";
import { fetchEntitySourcesWithCache } from "../../../data/entity_sources";
import type { UpdateEntity } from "../../../data/update";
import {
getUpdateType,
UpdateEntityFeature,
updateIsInstalling,
updateReleaseNotes,
@@ -33,6 +42,103 @@ class MoreInfoUpdate extends LitElement {
@state() private _markdownLoading = true;
@state() private _backupConfig?: BackupConfig;
@state() private _entitySources?: EntitySources;
private async _fetchBackupConfig() {
const { config } = await fetchBackupConfig(this.hass);
this._backupConfig = config;
}
private async _fetchEntitySources() {
this._entitySources = await fetchEntitySourcesWithCache(this.hass);
}
private _computeCreateBackupTexts():
| { title: string; description?: string }
| undefined {
if (
!this.stateObj ||
!supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
) {
return undefined;
}
const updateType = this._entitySources
? getUpdateType(this.stateObj, this._entitySources)
: "generic";
// Automatic or manual for Home Assistant update
if (updateType === "home_assistant") {
const isBackupConfigValid =
!!this._backupConfig &&
!!this._backupConfig.create_backup.password &&
this._backupConfig.create_backup.agent_ids.length > 0;
if (!isBackupConfigValid) {
return {
title: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.manual"
),
description: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.manual_description"
),
};
}
const lastAutomaticBackupDate = this._backupConfig
?.last_completed_automatic_backup
? new Date(this._backupConfig?.last_completed_automatic_backup)
: null;
const now = new Date();
return {
title: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.automatic"
),
description: lastAutomaticBackupDate
? this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.automatic_description_last",
{
relative_time: relativeTime(
lastAutomaticBackupDate,
this.hass.locale,
now,
true
),
}
)
: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.automatic_description_none"
),
};
}
// Addon backup
if (updateType === "addon") {
const version = this.stateObj.attributes.installed_version;
return {
title: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.addon"
),
description: version
? this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.addon_description",
{ version: version }
)
: undefined,
};
}
// Fallback to generic UI
return {
title: this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup.generic"
),
};
}
protected render() {
if (
!this.hass ||
@@ -47,6 +153,8 @@ class MoreInfoUpdate extends LitElement {
this.stateObj.attributes.skipped_version ===
this.stateObj.attributes.latest_version;
const createBackupTexts = this._computeCreateBackupTexts();
return html`
<div class="content">
<div class="summary">
@@ -133,6 +241,27 @@ class MoreInfoUpdate extends LitElement {
: nothing}
</div>
<div class="footer">
${createBackupTexts
? html`
<ha-md-list>
<ha-md-list-item>
<span slot="headline">${createBackupTexts.title}</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
id="create-backup"
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
</ha-md-list-item>
</ha-md-list>
`
: nothing}
<div class="actions">
${this.stateObj.state === BINARY_STATE_OFF &&
this.stateObj.attributes.skipped_version
@@ -186,6 +315,14 @@ class MoreInfoUpdate extends LitElement {
if (supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES)) {
this._fetchReleaseNotes();
}
if (supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
this._fetchEntitySources().then(() => {
const type = getUpdateType(this.stateObj!, this._entitySources!);
if (type === "home_assistant") {
this._fetchBackupConfig();
}
});
}
}
private async _markdownLoaded() {
@@ -205,11 +342,28 @@ class MoreInfoUpdate extends LitElement {
}
}
get _shouldCreateBackup(): boolean {
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
return false;
}
const createBackupSwitch = this.shadowRoot?.getElementById(
"create-backup"
) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
}
return false;
}
private _handleInstall(): void {
const installData: Record<string, any> = {
entity_id: this.stateObj!.entity_id,
};
if (this._shouldCreateBackup) {
installData.backup = true;
}
if (
supportsFeature(this.stateObj!, UpdateEntityFeature.SPECIFIC_VERSION) &&
this.stateObj!.attributes.latest_version
@@ -289,12 +443,20 @@ class MoreInfoUpdate extends LitElement {
z-index: 10;
}
ha-settings-row {
ha-md-list {
width: 100%;
padding: 0 24px;
box-sizing: border-box;
margin-bottom: -16px;
margin-top: -4px;
--md-sys-color-surface: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
}
ha-md-list-item {
--md-list-item-leading-space: 24px;
--md-list-item-trailing-space: 24px;
}
.actions {

View File

@@ -1,18 +1,13 @@
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import {
mdiEye,
mdiGauge,
mdiThermometer,
mdiWaterPercent,
mdiWeatherWindy,
} from "@mdi/js";
import type { PropertyValues } from "lit";
import { mdiEye, mdiGauge, mdiWaterPercent, mdiWeatherWindy } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { formatDateWeekdayDay } from "../../../common/datetime/format_date";
import { formatTimeWeekday } from "../../../common/datetime/format_time";
import { formatDateWeekdayShort } from "../../../common/datetime/format_date";
import { formatTime } from "../../../common/datetime/format_time";
import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-svg-icon";
import type {
ForecastEvent,
@@ -23,11 +18,16 @@ import {
getDefaultForecastType,
getForecast,
getSupportedForecastTypes,
getSecondaryWeatherAttribute,
getWeatherStateIcon,
getWeatherUnit,
getWind,
subscribeForecast,
weatherIcons,
weatherSVGStyles,
} from "../../../data/weather";
import type { HomeAssistant } from "../../../types";
import "../../../components/ha-relative-time";
import "../../../components/ha-state-icon";
@customElement("more-info-weather")
class MoreInfoWeather extends LitElement {
@@ -137,23 +137,90 @@ class MoreInfoWeather extends LitElement {
const hourly = forecastData?.type === "hourly";
const dayNight = forecastData?.type === "twice_daily";
const weatherStateIcon = getWeatherStateIcon(this.stateObj.state, this);
return html`
${this._showValue(this.stateObj.attributes.temperature)
? html`
<div class="flex">
<ha-svg-icon .path=${mdiThermometer}></ha-svg-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.temperature")}
</div>
<div>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"temperature"
)}
</div>
<div class="content">
<div class="icon-image">
${weatherStateIcon ||
html`
<ha-state-icon
class="weather-icon"
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-icon>
`}
</div>
<div class="info">
<div class="name-state">
<div class="state">
${this.hass.formatEntityState(this.stateObj)}
</div>
`
: ""}
<div class="time-ago">
<ha-relative-time
id="last_changed"
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time>
<simple-tooltip animation-delay="0" for="last_changed">
<div>
<div class="row">
<span class="column-name">
${this.hass.localize(
"ui.dialogs.more_info_control.last_changed"
)}:
</span>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time>
</div>
<div class="row">
<span>
${this.hass.localize(
"ui.dialogs.more_info_control.last_updated"
)}:
</span>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_updated}
capitalize
></ha-relative-time>
</div>
</div>
</simple-tooltip>
</div>
</div>
<div class="temp-attribute">
<div class="temp">
${this.stateObj.attributes.temperature !== undefined &&
this.stateObj.attributes.temperature !== null
? html`
${formatNumber(
this.stateObj.attributes.temperature,
this.hass.locale
)}&nbsp;<span
>${getWeatherUnit(
this.hass.config,
this.stateObj,
"temperature"
)}</span
>
`
: nothing}
</div>
<div class="attribute">
${getSecondaryWeatherAttribute(
this.hass,
this.stateObj,
forecast!
)}
</div>
</div>
</div>
</div>
${this._showValue(this.stateObj.attributes.pressure)
? html`
<div class="flex">
@@ -169,7 +236,7 @@ class MoreInfoWeather extends LitElement {
</div>
</div>
`
: ""}
: nothing}
${this._showValue(this.stateObj.attributes.humidity)
? html`
<div class="flex">
@@ -185,7 +252,7 @@ class MoreInfoWeather extends LitElement {
</div>
</div>
`
: ""}
: nothing}
${this._showValue(this.stateObj.attributes.wind_speed)
? html`
<div class="flex">
@@ -203,7 +270,7 @@ class MoreInfoWeather extends LitElement {
</div>
</div>
`
: ""}
: nothing}
${this._showValue(this.stateObj.attributes.visibility)
? html`
<div class="flex">
@@ -219,7 +286,7 @@ class MoreInfoWeather extends LitElement {
</div>
</div>
`
: ""}
: nothing}
${forecast
? html`
<div class="section">
@@ -242,76 +309,90 @@ class MoreInfoWeather extends LitElement {
)}
</mwc-tab-bar>`
: nothing}
${forecast.map((item) =>
this._showValue(item.templow) || this._showValue(item.temperature)
? html`<div class="flex">
${item.condition
? html`
<ha-svg-icon
.path=${weatherIcons[item.condition]}
></ha-svg-icon>
`
: ""}
<div class="main">
${dayNight
? html`
${formatDateWeekdayDay(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
(${item.is_daytime !== false
? this.hass!.localize("ui.card.weather.day")
: this.hass!.localize("ui.card.weather.night")})
`
: hourly
<div class="forecast">
${forecast.map((item) =>
this._showValue(item.templow) ||
this._showValue(item.temperature)
? html`
<div>
<div>
${dayNight
? html`
${formatDateWeekdayShort(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
<div class="daynight">
${item.is_daytime !== false
? this.hass!.localize("ui.card.weather.day")
: this.hass!.localize(
"ui.card.weather.night"
)}<br />
</div>
`
: hourly
? html`
${formatTime(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
`
: html`
${formatDateWeekdayShort(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
`}
</div>
${this._showValue(item.condition)
? html`
${formatTimeWeekday(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
<div class="forecast-image-icon">
${getWeatherStateIcon(
item.condition!,
this,
!(
item.is_daytime ||
item.is_daytime === undefined
)
)}
</div>
`
: html`
${formatDateWeekdayDay(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
`}
</div>
<div class="templow">
${this._showValue(item.templow)
? this.hass.formatEntityAttributeValue(
this.stateObj!,
"templow",
item.templow
)
: hourly
? ""
: "—"}
</div>
<div class="temp">
${this._showValue(item.temperature)
? this.hass.formatEntityAttributeValue(
this.stateObj!,
"temperature",
item.temperature
)
: "—"}
</div>
</div>`
: ""
)}
: nothing}
<div class="temp">
${this._showValue(item.temperature)
? html`${formatNumber(
item.temperature,
this.hass!.locale
)}°`
: "—"}
</div>
<div class="templow">
${this._showValue(item.templow)
? html`${formatNumber(
item.templow!,
this.hass!.locale
)}°`
: hourly
? nothing
: "—"}
</div>
</div>
`
: nothing
)}
</div>
`
: ""}
: nothing}
${this.stateObj.attributes.attribution
? html`
<div class="attribution">
${this.stateObj.attributes.attribution}
</div>
`
: ""}
: nothing}
`;
}
@@ -321,56 +402,186 @@ class MoreInfoWeather extends LitElement {
];
}
static styles = css`
ha-svg-icon {
color: var(--paper-item-icon-color);
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
}
static get styles(): CSSResultGroup {
return [
weatherSVGStyles,
css`
ha-svg-icon {
color: var(--paper-item-icon-color);
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
}
mwc-tab-bar {
margin-bottom: 4px;
}
mwc-tab-bar {
margin-bottom: 4px;
}
.section {
margin: 16px 0 8px 0;
font-size: 1.2em;
}
.section {
margin: 16px 0 8px 0;
font-size: 1.2em;
}
.flex {
display: flex;
height: 32px;
align-items: center;
}
.flex > div:last-child {
direction: ltr;
}
.flex {
display: flex;
height: 32px;
align-items: center;
}
.flex > div:last-child {
direction: ltr;
}
.main {
flex: 1;
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
}
.main {
flex: 1;
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
}
.temp,
.templow {
min-width: 48px;
text-align: right;
direction: ltr;
}
.attribution {
text-align: center;
margin-top: 16px;
}
.templow {
margin: 0 16px;
color: var(--secondary-text-color);
}
.time-ago,
.attribute {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.attribution {
color: var(--secondary-text-color);
text-align: center;
}
`;
.attribution,
.templow,
.daynight,
.attribute,
.time-ago {
color: var(--secondary-text-color);
}
.content {
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.icon-image {
display: flex;
align-items: center;
min-width: 64px;
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
}
.icon-image > * {
flex: 0 0 64px;
height: 64px;
}
.weather-icon {
--mdc-icon-size: 64px;
}
.info {
display: flex;
justify-content: space-between;
flex-grow: 1;
overflow: hidden;
}
.temp-attribute {
text-align: var(--float-end);
}
.temp-attribute .temp {
position: relative;
margin-right: 24px;
direction: ltr;
}
.temp-attribute .temp span {
position: absolute;
font-size: 24px;
top: 1px;
}
.state,
.temp-attribute .temp {
font-size: 28px;
line-height: 1.2;
}
.attribute {
font-size: 14px;
line-height: 1;
}
.name-state {
overflow: hidden;
padding-right: 12px;
padding-inline-end: 12px;
padding-inline-start: initial;
width: 100%;
}
.state {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.forecast {
display: flex;
justify-content: space-around;
padding: 16px;
padding-bottom: 0px;
overflow-x: auto;
scrollbar-color: var(--scrollbar-thumb-color) transparent;
scrollbar-width: thin;
mask-image: linear-gradient(
90deg,
transparent 0%,
black 5%,
black 94%,
transparent 100%
);
}
.forecast > div {
text-align: center;
padding: 0 10px;
}
.forecast .icon,
.forecast .temp {
margin: 4px 0;
}
.forecast .temp {
font-size: 16px;
}
.forecast-image-icon {
padding-top: 4px;
padding-bottom: 4px;
display: flex;
justify-content: center;
}
.forecast-image-icon > * {
width: 40px;
height: 40px;
--mdc-icon-size: 40px;
}
.forecast-icon {
--mdc-icon-size: 40px;
}
`,
];
}
private _showValue(item: number | string | undefined): boolean {
return typeof item !== "undefined" && item !== null;

View File

@@ -47,6 +47,8 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
@state() private _assistConfiguration?: AssistSatelliteConfiguration;
@state() private _error?: string;
private _previousSteps: STEP[] = [];
private _nextStep?: STEP;
@@ -165,79 +167,86 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
"update"
)}
></ha-voice-assistant-setup-step-update>`
: assistEntityState?.state === UNAVAILABLE
? this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.not_available"
)
: this._step === STEP.CHECK
? html`<ha-voice-assistant-setup-step-check
.hass=${this.hass}
.assistEntityId=${assistSatelliteEntityId}
></ha-voice-assistant-setup-step-check>`
: this._step === STEP.WAKEWORD
? html`<ha-voice-assistant-setup-step-wake-word
: this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: assistEntityState?.state === UNAVAILABLE
? html`<ha-alert alert-type="error"
>${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.not_available"
)}</ha-alert
>`
: this._step === STEP.CHECK
? html`<ha-voice-assistant-setup-step-check
.hass=${this.hass}
.assistConfiguration=${this._assistConfiguration}
.assistEntityId=${assistSatelliteEntityId}
.deviceEntities=${this._deviceEntities(
this._params.deviceId,
this.hass.entities
)}
></ha-voice-assistant-setup-step-wake-word>`
: this._step === STEP.CHANGE_WAKEWORD
? html`
<ha-voice-assistant-setup-step-change-wake-word
.hass=${this.hass}
.assistConfiguration=${this._assistConfiguration}
.assistEntityId=${assistSatelliteEntityId}
></ha-voice-assistant-setup-step-change-wake-word>
`
: this._step === STEP.AREA
></ha-voice-assistant-setup-step-check>`
: this._step === STEP.WAKEWORD
? html`<ha-voice-assistant-setup-step-wake-word
.hass=${this.hass}
.assistConfiguration=${this._assistConfiguration}
.assistEntityId=${assistSatelliteEntityId}
.deviceEntities=${this._deviceEntities(
this._params.deviceId,
this.hass.entities
)}
></ha-voice-assistant-setup-step-wake-word>`
: this._step === STEP.CHANGE_WAKEWORD
? html`
<ha-voice-assistant-setup-step-area
.hass=${this.hass}
.deviceId=${this._params.deviceId}
></ha-voice-assistant-setup-step-area>
`
: this._step === STEP.PIPELINE
? html`<ha-voice-assistant-setup-step-pipeline
<ha-voice-assistant-setup-step-change-wake-word
.hass=${this.hass}
.assistConfiguration=${this._assistConfiguration}
.assistEntityId=${assistSatelliteEntityId}
></ha-voice-assistant-setup-step-pipeline>`
: this._step === STEP.CLOUD
? html`<ha-voice-assistant-setup-step-cloud
></ha-voice-assistant-setup-step-change-wake-word>
`
: this._step === STEP.AREA
? html`
<ha-voice-assistant-setup-step-area
.hass=${this.hass}
></ha-voice-assistant-setup-step-cloud>`
: this._step === STEP.LOCAL
? html`<ha-voice-assistant-setup-step-local
.deviceId=${this._params.deviceId}
></ha-voice-assistant-setup-step-area>
`
: this._step === STEP.PIPELINE
? html`<ha-voice-assistant-setup-step-pipeline
.hass=${this.hass}
.assistConfiguration=${this._assistConfiguration}
.assistEntityId=${assistSatelliteEntityId}
></ha-voice-assistant-setup-step-pipeline>`
: this._step === STEP.CLOUD
? html`<ha-voice-assistant-setup-step-cloud
.hass=${this.hass}
.assistConfiguration=${this
._assistConfiguration}
></ha-voice-assistant-setup-step-local>`
: this._step === STEP.SUCCESS
? html`<ha-voice-assistant-setup-step-success
></ha-voice-assistant-setup-step-cloud>`
: this._step === STEP.LOCAL
? html`<ha-voice-assistant-setup-step-local
.hass=${this.hass}
.assistConfiguration=${this
._assistConfiguration}
.assistEntityId=${assistSatelliteEntityId}
></ha-voice-assistant-setup-step-success>`
: nothing}
></ha-voice-assistant-setup-step-local>`
: this._step === STEP.SUCCESS
? html`<ha-voice-assistant-setup-step-success
.hass=${this.hass}
.assistConfiguration=${this
._assistConfiguration}
.assistEntityId=${assistSatelliteEntityId}
></ha-voice-assistant-setup-step-success>`
: nothing}
</div>
</ha-dialog>
`;
}
private async _fetchAssistConfiguration() {
this._assistConfiguration = await fetchAssistSatelliteConfiguration(
this.hass,
this._findDomainEntityId(
this._params!.deviceId,
this.hass.entities,
"assist_satellite"
)!
);
return this._assistConfiguration;
try {
this._assistConfiguration = await fetchAssistSatelliteConfiguration(
this.hass,
this._findDomainEntityId(
this._params!.deviceId,
this.hass.entities,
"assist_satellite"
)!
);
} catch (err: any) {
this._error = err.message;
}
}
private _goToPreviousStep() {
@@ -293,6 +302,10 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
.skip-btn {
margin-top: 6px;
}
ha-alert {
margin: 24px;
display: block;
}
`,
];
}

View File

@@ -85,7 +85,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
<div class="rows">
${this.assistConfiguration &&
this.assistConfiguration.available_wake_words.length > 1
? html` <div class="row">
? html`<div class="row">
<ha-select
.label=${"Wake word"}
@closed=${stopPropagation}

View File

@@ -44,6 +44,15 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
protected override willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (changedProperties.has("assistConfiguration")) {
if (
this.assistConfiguration &&
!this.assistConfiguration.available_wake_words.length
) {
this._nextStep();
}
}
if (changedProperties.has("assistEntityId")) {
this._detected = false;
this._muteSwitchEntity = this.deviceEntities?.find(
@@ -135,13 +144,16 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
>`
: nothing}
</div>
<div class="footer centered">
<ha-button @click=${this._changeWakeWord}
>${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.wake_word.change_wake_word"
)}</ha-button
>
</div>`;
${this.assistConfiguration &&
this.assistConfiguration.available_wake_words.length > 1
? html`<div class="footer centered">
<ha-button @click=${this._changeWakeWord}
>${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.wake_word.change_wake_word"
)}</ha-button
>
</div>`
: nothing}`;
}
private async _listenWakeWord() {

View File

@@ -20,6 +20,12 @@
<meta name="color-scheme" content="dark light" />
<%= renderTemplate("_style_base.html.template") %>
<style>
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
html {
background-color: var(--primary-background-color, #fafafa);
color: var(--primary-text-color, #212121);

View File

@@ -95,7 +95,10 @@ export class HassRouterPage extends ReactiveElement {
const defaultPage = routerOptions.defaultPage;
if (route && route.path === "" && defaultPage !== undefined) {
navigate(`${route.prefix}/${defaultPage}`, { replace: true });
const queryParams = window.location.search;
navigate(`${route.prefix}/${defaultPage}${queryParams}`, {
replace: true,
});
}
let newPage = route

View File

@@ -5,7 +5,7 @@ import {
mdiArrowDown,
mdiArrowUp,
mdiClose,
mdiCog,
mdiTableCog,
mdiFilterVariant,
mdiFilterVariantRemove,
mdiFormatListChecks,
@@ -309,7 +309,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
@click=${this._openSettings}
.title=${localize("ui.components.subpage-data-table.settings")}
>
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
<ha-svg-icon slot="icon" .path=${mdiTableCog}></ha-svg-icon>
</ha-assist-chip>`;
return html`
@@ -355,7 +355,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
></ha-assist-chip>
<ha-md-menu-item
.value=${undefined}
@click=${this._selectAll}
.clickAction=${this._selectAll}
>
<div slot="headline">
${localize("ui.components.subpage-data-table.select_all")}
@@ -363,7 +363,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
</ha-md-menu-item>
<ha-md-menu-item
.value=${undefined}
@click=${this._selectNone}
.clickAction=${this._selectNone}
>
<div slot="headline">
${localize(
@@ -374,7 +374,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.value=${undefined}
@click=${this._disableSelectMode}
.clickAction=${this._disableSelectMode}
>
<div slot="headline">
${localize(
@@ -500,7 +500,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
? html`
<ha-md-menu-item
.value=${id}
@click=${this._handleGroupBy}
.clickAction=${this._handleGroupBy}
.selected=${id === this._groupColumn}
class=${classMap({ selected: id === this._groupColumn })}
>
@@ -511,7 +511,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
)}
<ha-md-menu-item
.value=${undefined}
@click=${this._handleGroupBy}
.clickAction=${this._handleGroupBy}
.selected=${this._groupColumn === undefined}
class=${classMap({ selected: this._groupColumn === undefined })}
>
@@ -519,7 +519,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
@click=${this._collapseAllGroups}
.clickAction=${this._collapseAllGroups}
.disabled=${this._groupColumn === undefined}
>
<ha-svg-icon
@@ -529,7 +529,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
${localize("ui.components.subpage-data-table.collapse_all_groups")}
</ha-md-menu-item>
<ha-md-menu-item
@click=${this._expandAllGroups}
.clickAction=${this._expandAllGroups}
.disabled=${this._groupColumn === undefined}
>
<ha-svg-icon
@@ -546,6 +546,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
<ha-md-menu-item
.value=${id}
@click=${this._handleSortBy}
@keydown=${this._handleSortBy}
keep-open
.selected=${id === this._sortColumn}
class=${classMap({ selected: id === this._sortColumn })}
@@ -623,6 +624,8 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
}
private _handleSortBy(ev) {
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") return;
const columnId = ev.currentTarget.value;
if (!this._sortDirection || this._sortColumn !== columnId) {
this._sortDirection = "asc";
@@ -639,9 +642,9 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
});
}
private _handleGroupBy(ev) {
this._setGroupColumn(ev.currentTarget.value);
}
private _handleGroupBy = (item) => {
this._setGroupColumn(item.value);
};
private _setGroupColumn(columnId: string) {
this._groupColumn = columnId;
@@ -665,30 +668,30 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
});
}
private _collapseAllGroups() {
private _collapseAllGroups = () => {
this._dataTable.collapseAllGroups();
}
};
private _expandAllGroups() {
private _expandAllGroups = () => {
this._dataTable.expandAllGroups();
}
};
private _enableSelectMode() {
this._selectMode = true;
}
private _disableSelectMode() {
private _disableSelectMode = () => {
this._selectMode = false;
this._dataTable.clearSelection();
}
};
private _selectAll() {
private _selectAll = () => {
this._dataTable.selectAll();
}
};
private _selectNone() {
private _selectNone = () => {
this._dataTable.clearSelection();
}
};
private _handleSearchChange(ev: CustomEvent) {
if (this.filter === ev.detail.value) {

View File

@@ -17,7 +17,7 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
@property({ attribute: false }) public localize: LocalizeFunc = empty;
// Use browser language setup before login.
@property() public language?: string = getLocalLanguage();
@property() public language: string = getLocalLanguage();
@property() public translationFragment?: string;

View File

@@ -41,6 +41,7 @@ import "./onboarding-analytics";
import "./onboarding-create-user";
import "./onboarding-loading";
import "./onboarding-welcome";
import "./onboarding-restore-backup";
import "./onboarding-welcome-links";
import { makeDialogManager } from "../dialogs/make-dialog-manager";
import { navigate } from "../common/navigate";
@@ -157,8 +158,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
private _renderStep() {
if (this._restoring) {
return html`<onboarding-restore-backup
.hass=${this.hass}
.localize=${this.localize}
.supervisor=${this._supervisor ?? false}
.language=${this.language}
>
</onboarding-restore-backup>`;
}
@@ -166,8 +168,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
if (this._init) {
return html`<onboarding-welcome
.localize=${this.localize}
.language=${this.language}
.supervisor=${this._supervisor}
></onboarding-welcome>`;
}
@@ -236,7 +236,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
}
}
if (changedProps.has("language")) {
document.querySelector("html")!.setAttribute("lang", this.language!);
document.querySelector("html")!.setAttribute("lang", this.language);
}
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
@@ -272,10 +272,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
"Home Assistant OS",
"Home Assistant Supervised",
].includes(response.installation_type);
if (this._supervisor) {
// Only load if we have supervisor
import("./onboarding-restore-backup");
}
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(
@@ -454,7 +450,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
subscribeOne(conn, subscribeUser),
]);
this.initializeHass(auth, conn);
if (this.language && this.language !== this.hass!.language) {
if (this.language !== this.hass!.language) {
this._updateHass({
locale: { ...this.hass!.locale, language: this.language },
language: this.language,

View File

@@ -1,136 +1,336 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
import "../../hassio/src/components/hassio-upload-backup";
import "./restore-backup/onboarding-restore-backup-upload";
import "./restore-backup/onboarding-restore-backup-details";
import "./restore-backup/onboarding-restore-backup-restore";
import "./restore-backup/onboarding-restore-backup-status";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-ansi-to-html";
import "../components/ha-card";
import "../components/ha-icon-button-arrow-prev";
import "../components/ha-circular-progress";
import "../components/ha-alert";
import "../components/ha-button";
import { fetchInstallationType } from "../data/onboarding";
import type { HomeAssistant } from "../types";
import "./onboarding-loading";
import { onBoardingStyles } from "./styles";
import { removeSearchParam } from "../common/url/search-params";
import { navigate } from "../common/navigate";
import { onBoardingStyles } from "./styles";
import {
fetchBackupOnboardingInfo,
type BackupOnboardingConfig,
type BackupOnboardingInfo,
} from "../data/backup_onboarding";
import type { BackupContentExtended, BackupData } from "../data/backup";
import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
import { storage } from "../common/decorators/storage";
const STATUS_INTERVAL_IN_MS = 5000;
@customElement("onboarding-restore-backup")
class OnboardingRestoreBackup extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public localize!: LocalizeFunc;
@property() public language!: string;
@state() private _restoring = false;
@property({ type: Boolean }) public supervisor = false;
@state() private _backupSlug?: string;
@state() private _view:
| "loading"
| "upload"
| "select_data"
| "confirm_restore"
| "status" = "loading";
@state() private _backup?: BackupContentExtended;
@state() private _backupInfo?: BackupOnboardingInfo;
@state() private _selectedData?: BackupData;
@state() private _error?: string;
@state() private _failed?: boolean;
@storage({
key: "onboarding-restore-backup-backup-id",
})
private _backupId?: string;
@storage({
key: "onboarding-restore-running",
})
private _restoreRunning?: boolean;
protected render(): TemplateResult {
return html`
${this._restoring
? html`<h1>
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
</h1>
<ha-alert alert-type="info">
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
</ha-alert>
<onboarding-loading></onboarding-loading>`
: html` <h1>
${this.localize("ui.panel.page-onboarding.restore.header")}
</h1>
<hassio-upload-backup
@backup-uploaded=${this._backupUploaded}
@backup-cleared=${this._backupCleared}
.hass=${this.hass}
.localize=${this.localize}
></hassio-upload-backup>`}
<div class="footer">
<ha-button @click=${this._back} .disabled=${this._restoring}>
${this.localize("ui.panel.page-onboarding.back")}
</ha-button>
${this._backupSlug
? html`<ha-button
@click=${this._showBackupDialog}
.disabled=${this._restoring}
${
this._view !== "status" || this._failed
? html`<ha-icon-button-arrow-prev
.label=${this.localize("ui.panel.page-onboarding.restore.back")}
@click=${this._back}
></ha-icon-button-arrow-prev>`
: nothing
}
</ha-icon-button>
<h1>${this.localize("ui.panel.page-onboarding.restore.header")}</h1>
${
this._error || (this._failed && this._view !== "status")
? html`<ha-alert
alert-type="error"
.title=${this._failed && this._view !== "status"
? this.localize("ui.panel.page-onboarding.restore.failed")
: ""}
>
${this.localize("ui.panel.page-onboarding.restore.restore")}
</ha-button>`
: nothing}
</div>
${this._failed && this._view !== "status"
? this.localize(
`ui.panel.page-onboarding.restore.${this._backupInfo?.last_non_idle_event?.reason === "password_incorrect" ? "failed_wrong_password_description" : "failed_description"}`
)
: this._error}
</ha-alert>`
: nothing
}
${
this._view === "loading"
? html`<div class="loading">
<ha-circular-progress indeterminate></ha-circular-progress>
</div>`
: this._view === "upload"
? html`
<onboarding-restore-backup-upload
.supervisor=${this.supervisor}
.localize=${this.localize}
@backup-uploaded=${this._backupUploaded}
></onboarding-restore-backup-upload>
`
: this._view === "select_data"
? html`<onboarding-restore-backup-details
.localize=${this.localize}
.backup=${this._backup!}
@backup-restore=${this._restore}
></onboarding-restore-backup-details>`
: this._view === "confirm_restore"
? html`<onboarding-restore-backup-restore
.localize=${this.localize}
.backup=${this._backup!}
.supervisor=${this.supervisor}
.selectedData=${this._selectedData!}
@restore-started=${this._restoreStarted}
></onboarding-restore-backup-restore>`
: nothing
}
${
this._view === "status" && this._backupInfo
? html`<onboarding-restore-backup-status
.localize=${this.localize}
.backupInfo=${this._backupInfo}
@show-backup-upload=${this._reupload}
></onboarding-restore-backup-status>`
: nothing
}
${
["select_data", "confirm_restore"].includes(this._view) && this._backup
? html`<div class="backup-summary-wrapper">
<ha-backup-details-summary
translation-key-panel="page-onboarding.restore"
show-upload-another
.backup=${this._backup}
.localize=${this.localize}
@show-backup-upload=${this._reupload}
.isHassio=${this.supervisor}
></ha-backup-details-summary>
</div>`
: nothing
}
`;
}
private _back(): void {
navigate(`${location.pathname}?${removeSearchParam("page")}`);
}
private _backupUploaded(ev) {
const backup = ev.detail.backup;
this._backupSlug = backup.slug;
this._showBackupDialog();
}
private _backupCleared() {
this._backupSlug = undefined;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._loadBackupInfo();
}
private async _checkRestoreStatus(): Promise<void> {
if (this._restoring) {
try {
await fetchInstallationType();
} catch (err: any) {
private async _loadBackupInfo() {
let onboardingInfo: BackupOnboardingConfig;
try {
onboardingInfo = await fetchBackupOnboardingInfo();
} catch (err: any) {
if (this._restoreRunning) {
if (
(err as Error).message === "unauthorized" ||
(err as Error).message === "not_found"
err.error === "Request error" ||
// core can restart but haven't loaded the backup integration yet
(err.status_code === 500 && err.body?.error === "backup_disabled")
) {
// core is down because of restore, keep trying
this._scheduleLoadBackupInfo();
return;
}
// core seems to be back up restored
if (err.status_code === 404) {
this._restoreRunning = undefined;
this._backupId = undefined;
window.location.replace("/");
return;
}
}
this._error = err?.message || "Cannot get backup info";
// if we are in an unknown state, show upload
if (this._view === "loading") {
this._view = "upload";
}
return;
}
const {
last_non_idle_event: lastNonIdleEvent,
state: currentState,
backups,
} = onboardingInfo;
this._backupInfo = {
state: currentState,
last_non_idle_event: lastNonIdleEvent,
};
if (this._backupId) {
this._backup = backups.find(
({ backup_id }) => backup_id === this._backupId
);
}
const failedRestore =
lastNonIdleEvent?.manager_state === "restore_backup" &&
lastNonIdleEvent?.state === "failed";
if (failedRestore) {
this._failed = true;
}
if (this._restoreRunning) {
this._view = "status";
if (failedRestore || currentState !== "restore_backup") {
this._failed = true;
this._restoreRunning = undefined;
} else {
this._scheduleLoadBackupInfo();
}
return;
}
if (
this._backup &&
// after backup was uploaded
(lastNonIdleEvent?.manager_state === "receive_backup" ||
// when restore was confirmed but failed to start (for example, encryption key was wrong)
failedRestore)
) {
if (!this.supervisor && this._backup.homeassistant_included) {
this._selectedData = {
homeassistant_included: true,
folders: [],
addons: [],
homeassistant_version: this._backup.homeassistant_version,
database_included: this._backup.database_included,
};
// skip select data when supervisor is not available and backup includes HA
this._view = "confirm_restore";
} else {
this._view = "select_data";
}
return;
}
// show upload as default
this._view = "upload";
}
private _scheduleLoadBackupInfo() {
setTimeout(() => this._loadBackupInfo(), STATUS_INTERVAL_IN_MS);
}
private async _backupUploaded(ev: CustomEvent) {
this._backupId = ev.detail.backupId;
await this._loadBackupInfo();
}
private async _restoreStarted() {
if (this._backupInfo) {
this._backupInfo.state = "restore_backup";
}
this._view = "status";
this._restoreRunning = true;
await this._loadBackupInfo();
}
private async _back() {
if (this._view === "upload" || (this._view === "status" && this._failed)) {
navigate(`${location.pathname}?${removeSearchParam("page")}`);
} else {
const confirmed = await showConfirmationDialog(this, {
title: this.localize(
"ui.panel.page-onboarding.restore.cancel_restore.title"
),
text: this.localize(
"ui.panel.page-onboarding.restore.cancel_restore.text"
),
confirmText: this.localize(
"ui.panel.page-onboarding.restore.cancel_restore.yes"
),
dismissText: this.localize(
"ui.panel.page-onboarding.restore.cancel_restore.no"
),
});
if (!confirmed) {
return;
}
navigate(`${location.pathname}?${removeSearchParam("page")}`);
}
}
private _scheduleCheckRestoreStatus(): void {
setTimeout(() => this._checkRestoreStatus(), 1000);
private _restore(ev: CustomEvent) {
if (!this._backup || !ev.detail.selectedData) {
return;
}
this._selectedData = ev.detail.selectedData;
this._view = "confirm_restore";
}
private _showBackupDialog(): void {
showHassioBackupDialog(this, {
slug: this._backupSlug!,
onboarding: true,
localize: this.localize,
onRestoring: () => {
this._restoring = true;
this._scheduleCheckRestoreStatus();
},
});
private _reupload() {
this._backup = undefined;
this._backupId = undefined;
this._view = "upload";
}
static get styles(): CSSResultGroup {
return [
onBoardingStyles,
css`
:host {
display: flex;
flex-direction: column;
align-items: center;
}
hassio-upload-backup {
width: 100%;
}
.footer {
display: flex;
justify-content: space-between;
width: 100%;
}
`,
];
}
static styles = [
onBoardingStyles,
css`
:host {
display: flex;
flex-direction: column;
position: relative;
}
ha-icon-button-arrow-prev {
position: absolute;
top: 12px;
}
ha-card {
width: 100%;
}
.loading {
display: flex;
justify-content: center;
padding: 32px;
}
.backup-summary-wrapper {
margin-top: 24px;
padding: 0 20px;
}
`,
];
}
declare global {

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